{"id":86703,"date":"2026-07-29T04:32:41","date_gmt":"2026-07-28T20:32:41","guid":{"rendered":"https:\/\/www.wsisp.com\/helps\/86703.html"},"modified":"2026-07-29T04:32:41","modified_gmt":"2026-07-28T20:32:41","slug":"http-%e7%8a%b6%e6%80%81%e7%a0%81%ef%bc%9a%e5%ae%a2%e6%88%b7%e7%ab%af%e4%b8%8e%e6%9c%8d%e5%8a%a1%e5%99%a8%e7%9a%84%e9%80%9a%e4%bf%a1%e8%af%ad%e8%a8%80-%e7%ac%ac%e5%85%ad%e9%83%a8-2","status":"publish","type":"post","link":"https:\/\/www.wsisp.com\/helps\/86703.html","title":{"rendered":"HTTP \u72b6\u6001\u7801\uff1a\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u901a\u4fe1\u8bed\u8a00\u2014\u2014\u7b2c\u516d\u90e8\u5206\uff1a\u72b6\u6001\u7801\u7684\u5b9e\u8df5\u5e94\u7528\uff08\u4e00\uff09"},"content":{"rendered":"<h3>\u7b2c30\u7ae0&#xff1a;RESTful API\u8bbe\u8ba1\u4e2d\u7684\u72b6\u6001\u7801\u4f7f\u7528<\/h3>\n<h4>30.1 RESTful\u67b6\u6784\u4e0e\u72b6\u6001\u7801\u7684\u54f2\u5b66<\/h4>\n<p>RESTful API\u8bbe\u8ba1\u4e2d&#xff0c;HTTP\u72b6\u6001\u7801\u4e0d\u4ec5\u662f\u6280\u672f\u5b9e\u73b0\u7ec6\u8282&#xff0c;\u66f4\u662fAPI\u5951\u7ea6\u7684\u91cd\u8981\u7ec4\u6210\u90e8\u5206\u3002\u5b83\u4eec\u6784\u6210\u4e86\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u4e4b\u95f4\u901a\u4fe1\u7684\u8bed\u4e49\u6846\u67b6&#xff0c;\u4f7f\u5f97API\u80fd\u591f\u81ea\u6211\u63cf\u8ff0\u5176\u5904\u7406\u7ed3\u679c\u3002<\/p>\n<h5>30.1.1 Richardson\u6210\u719f\u5ea6\u6a21\u578b\u4e0e\u72b6\u6001\u7801<\/h5>\n<p>Richardson\u6210\u719f\u5ea6\u6a21\u578b\u5c06RESTful\u670d\u52a1\u5206\u4e3a\u56db\u4e2a\u7b49\u7ea7&#xff1a;<\/p>\n<p>Level 0&#xff1a;POX\u6cbc\u6cfd<\/p>\n<ul>\n<li>\n<p>\u4f7f\u7528\u5355\u4e00\u7aef\u70b9&#xff08;\u5982\/api&#xff09;<\/p>\n<\/li>\n<li>\n<p>\u5355\u4e00HTTP\u65b9\u6cd5&#xff08;\u901a\u5e38POST&#xff09;<\/p>\n<\/li>\n<li>\n<p>\u72b6\u6001\u4fe1\u606f\u5728\u54cd\u5e94\u4f53\u4e2d\u4f20\u9012<\/p>\n<\/li>\n<li>\n<p>\u72b6\u6001\u7801\u57fa\u672c\u53ea\u6709200\u548c500<\/p>\n<\/li>\n<\/ul>\n<p>http<\/p>\n<p>POST \/api HTTP\/1.1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;operation&#034;: &#034;createUser&#034;,<br \/>\n  &#034;data&#034;: {&#034;name&#034;: &#034;John&#034;, &#034;email&#034;: &#034;john&#064;example.com&#034;}<br \/>\n}<\/p>\n<p>HTTP\/1.1 200 OK<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;status&#034;: &#034;error&#034;,<br \/>\n  &#034;code&#034;: &#034;USER_EXISTS&#034;,<br \/>\n  &#034;message&#034;: &#034;User already exists&#034;<br \/>\n}<\/p>\n<p>Level 1&#xff1a;\u8d44\u6e90\u5206\u79bb<\/p>\n<ul>\n<li>\n<p>\u591a\u4e2a\u7aef\u70b9\u5bf9\u5e94\u4e0d\u540c\u8d44\u6e90<\/p>\n<\/li>\n<li>\n<p>\u4ecd\u4e3b\u8981\u4f7f\u7528\u5355\u4e00HTTP\u65b9\u6cd5<\/p>\n<\/li>\n<li>\n<p>\u72b6\u6001\u7801\u5f00\u59cb\u6709\u66f4\u591a\u5e94\u7528<\/p>\n<\/li>\n<\/ul>\n<p>Level 2&#xff1a;HTTP\u52a8\u8bcd<\/p>\n<ul>\n<li>\n<p>\u6b63\u786e\u4f7f\u7528HTTP\u65b9\u6cd5&#xff08;GET\u3001POST\u3001PUT\u3001DELETE\u7b49&#xff09;<\/p>\n<\/li>\n<li>\n<p>\u72b6\u6001\u7801\u88ab\u5145\u5206\u5229\u7528<\/p>\n<\/li>\n<li>\n<p>\u8fd9\u662f\u5927\u591a\u6570&#034;RESTful&#034;API\u6240\u5728\u7684\u5c42\u7ea7<\/p>\n<\/li>\n<\/ul>\n<p>Level 3&#xff1a;\u8d85\u5a92\u4f53\u63a7\u5236&#xff08;HATEOAS&#xff09;<\/p>\n<ul>\n<li>\n<p>\u54cd\u5e94\u4e2d\u5305\u542b\u76f8\u5173\u64cd\u4f5c\u7684\u94fe\u63a5<\/p>\n<\/li>\n<li>\n<p>\u72b6\u6001\u7801\u5f15\u5bfc\u5ba2\u6237\u7aef\u72b6\u6001\u8f6c\u79fb<\/p>\n<\/li>\n<li>\n<p>\u771f\u6b63\u7684RESTful\u5b9e\u73b0<\/p>\n<\/li>\n<\/ul>\n<p>http<\/p>\n<p>GET \/orders\/123 HTTP\/1.1<br \/>\nAccept: application\/json<\/p>\n<p>HTTP\/1.1 200 OK<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;id&#034;: &#034;123&#034;,<br \/>\n  &#034;status&#034;: &#034;processing&#034;,<br \/>\n  &#034;total&#034;: 99.99,<br \/>\n  &#034;_links&#034;: {<br \/>\n    &#034;self&#034;: { &#034;href&#034;: &#034;\/orders\/123&#034; },<br \/>\n    &#034;cancel&#034;: {<br \/>\n      &#034;href&#034;: &#034;\/orders\/123\/cancel&#034;,<br \/>\n      &#034;method&#034;: &#034;POST&#034;<br \/>\n    },<br \/>\n    &#034;payment&#034;: {<br \/>\n      &#034;href&#034;: &#034;\/orders\/123\/payment&#034;,<br \/>\n      &#034;method&#034;: &#034;POST&#034;<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<h4>30.2 RESTful API\u72b6\u6001\u7801\u9009\u62e9\u6307\u5357<\/h4>\n<h5>30.2.1 \u6210\u529f\u7c7b\u72b6\u6001\u7801&#xff08;2xx&#xff09;<\/h5>\n<p>200 OK &#8211; \u901a\u7528\u6210\u529f<\/p>\n<p>http<\/p>\n<p>GET \/users\/123 HTTP\/1.1<\/p>\n<p>HTTP\/1.1 200 OK<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;id&#034;: &#034;123&#034;,<br \/>\n  &#034;name&#034;: &#034;John Doe&#034;,<br \/>\n  &#034;email&#034;: &#034;john&#064;example.com&#034;<br \/>\n}<\/p>\n<p>\u9002\u7528\u573a\u666f&#xff1a;<\/p>\n<ul>\n<li>\n<p>GET&#xff1a;\u8d44\u6e90\u83b7\u53d6\u6210\u529f<\/p>\n<\/li>\n<li>\n<p>PUT\/PATCH&#xff1a;\u8d44\u6e90\u66f4\u65b0\u6210\u529f<\/p>\n<\/li>\n<li>\n<p>POST&#xff1a;\u975e\u521b\u5efa\u64cd\u4f5c\u6210\u529f&#xff08;\u5982\u8ba1\u7b97\u3001\u9a8c\u8bc1&#xff09;<\/p>\n<\/li>\n<\/ul>\n<p>201 Created &#8211; \u8d44\u6e90\u521b\u5efa\u6210\u529f<\/p>\n<p>http<\/p>\n<p>POST \/users HTTP\/1.1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;name&#034;: &#034;Jane Smith&#034;,<br \/>\n  &#034;email&#034;: &#034;jane&#064;example.com&#034;<br \/>\n}<\/p>\n<p>HTTP\/1.1 201 Created<br \/>\nLocation: \/users\/456<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;id&#034;: &#034;456&#034;,<br \/>\n  &#034;name&#034;: &#034;Jane Smith&#034;,<br \/>\n  &#034;email&#034;: &#034;jane&#064;example.com&#034;,<br \/>\n  &#034;created_at&#034;: &#034;2024-01-15T10:30:00Z&#034;<br \/>\n}<\/p>\n<p>\u6700\u4f73\u5b9e\u8df5&#xff1a;<\/p>\n<li>\n<p>\u5fc5\u987b\u5305\u542bLocation\u5934\u6307\u5411\u65b0\u8d44\u6e90<\/p>\n<\/li>\n<li>\n<p>\u54cd\u5e94\u4f53\u5e94\u5305\u542b\u521b\u5efa\u7684\u8d44\u6e90\u8868\u793a<\/p>\n<\/li>\n<li>\n<p>\u9002\u7528\u4e8e\u540c\u6b65\u521b\u5efa\u64cd\u4f5c<\/p>\n<\/li>\n<p>202 Accepted &#8211; \u8bf7\u6c42\u5df2\u63a5\u53d7<\/p>\n<p>http<\/p>\n<p>POST \/video-transcoding HTTP\/1.1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;video_id&#034;: &#034;789&#034;,<br \/>\n  &#034;format&#034;: &#034;mp4&#034;<br \/>\n}<\/p>\n<p>HTTP\/1.1 202 Accepted<br \/>\nLocation: \/jobs\/transcode-abc123<br \/>\nRetry-After: 30<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;job_id&#034;: &#034;transcode-abc123&#034;,<br \/>\n  &#034;status&#034;: &#034;pending&#034;,<br \/>\n  &#034;estimated_completion&#034;: &#034;2024-01-15T10:35:00Z&#034;,<br \/>\n  &#034;_links&#034;: {<br \/>\n    &#034;self&#034;: { &#034;href&#034;: &#034;\/jobs\/transcode-abc123&#034; },<br \/>\n    &#034;status&#034;: { &#034;href&#034;: &#034;\/jobs\/transcode-abc123\/status&#034; }<br \/>\n  }<br \/>\n}<\/p>\n<p>\u9002\u7528\u573a\u666f&#xff1a;<\/p>\n<ul>\n<li>\n<p>\u5f02\u6b65\u5904\u7406<\/p>\n<\/li>\n<li>\n<p>\u957f\u65f6\u95f4\u8fd0\u884c\u7684\u64cd\u4f5c<\/p>\n<\/li>\n<li>\n<p>\u9700\u8981\u8f6e\u8be2\u7ed3\u679c\u7684\u60c5\u51b5<\/p>\n<\/li>\n<\/ul>\n<p>204 No Content &#8211; \u65e0\u5185\u5bb9<\/p>\n<p>http<\/p>\n<p>DELETE \/users\/123 HTTP\/1.1<\/p>\n<p>HTTP\/1.1 204 No Content<\/p>\n<p>\u9002\u7528\u573a\u666f&#xff1a;<\/p>\n<ul>\n<li>\n<p>DELETE\u64cd\u4f5c\u6210\u529f<\/p>\n<\/li>\n<li>\n<p>PUT\/PATCH\u66f4\u65b0\u4f46\u65e0\u9700\u8fd4\u56de\u8d44\u6e90<\/p>\n<\/li>\n<li>\n<p>\u67d0\u4e9bPOST\u64cd\u4f5c&#xff08;\u5982\u70b9\u8d5e\u3001\u5173\u6ce8&#xff09;<\/p>\n<\/li>\n<\/ul>\n<h5>30.2.2 \u91cd\u5b9a\u5411\u7c7b\u72b6\u6001\u7801&#xff08;3xx&#xff09;<\/h5>\n<p>301 Moved Permanently \u548c 308 Permanent Redirect<\/p>\n<p>http<\/p>\n<p>GET \/old-api\/users HTTP\/1.1<\/p>\n<p>HTTP\/1.1 301 Moved Permanently<br \/>\nLocation: \/api\/v2\/users<\/p>\n<p>307 Temporary Redirect \u548c 302 Found<\/p>\n<p>http<\/p>\n<p>POST \/api\/maintenance HTTP\/1.1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;action&#034;: &#034;backup&#034;<br \/>\n}<\/p>\n<p>HTTP\/1.1 307 Temporary Redirect<br \/>\nLocation: \/api\/backup-endpoint<\/p>\n<p>303 See Other &#8211; POST\u540e\u7684\u91cd\u5b9a\u5411<\/p>\n<p>http<\/p>\n<p>POST \/checkout HTTP\/1.1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;cart_id&#034;: &#034;cart123&#034;<br \/>\n}<\/p>\n<p>HTTP\/1.1 303 See Other<br \/>\nLocation: \/order\/456\/confirmation<\/p>\n<h5>30.2.3 \u5ba2\u6237\u7aef\u9519\u8bef\u7c7b\u72b6\u6001\u7801&#xff08;4xx&#xff09;<\/h5>\n<p>400 Bad Request &#8211; \u901a\u7528\u5ba2\u6237\u7aef\u9519\u8bef<\/p>\n<p>http<\/p>\n<p>POST \/users HTTP\/1.1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;name&#034;: &#034;John&#034;,<br \/>\n  &#034;email&#034;: &#034;invalid-email&#034;<br \/>\n}<\/p>\n<p>HTTP\/1.1 400 Bad Request<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;VALIDATION_ERROR&#034;,<br \/>\n    &#034;message&#034;: &#034;Request validation failed&#034;,<br \/>\n    &#034;details&#034;: [<br \/>\n      {<br \/>\n        &#034;field&#034;: &#034;email&#034;,<br \/>\n        &#034;message&#034;: &#034;Must be a valid email address&#034;<br \/>\n      }<br \/>\n    ]<br \/>\n  }<br \/>\n}<\/p>\n<p>401 Unauthorized &#8211; \u8ba4\u8bc1\u5931\u8d25<\/p>\n<p>http<\/p>\n<p>GET \/secure-data HTTP\/1.1<br \/>\nAuthorization: Bearer expired-token<\/p>\n<p>HTTP\/1.1 401 Unauthorized<br \/>\nWWW-Authenticate: Bearer realm&#061;&#034;api&#034;, error&#061;&#034;invalid_token&#034;<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;INVALID_TOKEN&#034;,<br \/>\n    &#034;message&#034;: &#034;The access token is expired or invalid&#034;<br \/>\n  }<br \/>\n}<\/p>\n<p>403 Forbidden &#8211; \u6743\u9650\u4e0d\u8db3<\/p>\n<p>http<\/p>\n<p>GET \/admin\/users HTTP\/1.1<br \/>\nAuthorization: Bearer user-token<\/p>\n<p>HTTP\/1.1 403 Forbidden<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;INSUFFICIENT_PERMISSIONS&#034;,<br \/>\n    &#034;message&#034;: &#034;You don&#039;t have permission to access this resource&#034;<br \/>\n  }<br \/>\n}<\/p>\n<p>404 Not Found &#8211; \u8d44\u6e90\u4e0d\u5b58\u5728<\/p>\n<p>http<\/p>\n<p>GET \/users\/99999 HTTP\/1.1<\/p>\n<p>HTTP\/1.1 404 Not Found<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;RESOURCE_NOT_FOUND&#034;,<br \/>\n    &#034;message&#034;: &#034;User with id 99999 not found&#034;,<br \/>\n    &#034;resource&#034;: &#034;user&#034;,<br \/>\n    &#034;resource_id&#034;: &#034;99999&#034;<br \/>\n  }<br \/>\n}<\/p>\n<p>405 Method Not Allowed<\/p>\n<p>http<\/p>\n<p>PUT \/users HTTP\/1.1<\/p>\n<p>HTTP\/1.1 405 Method Not Allowed<br \/>\nAllow: GET, POST, HEAD, OPTIONS<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;METHOD_NOT_ALLOWED&#034;,<br \/>\n    &#034;message&#034;: &#034;PUT method is not allowed for this resource&#034;<br \/>\n  }<br \/>\n}<\/p>\n<p>409 Conflict &#8211; \u8d44\u6e90\u51b2\u7a81<\/p>\n<p>http<\/p>\n<p>PUT \/users\/123 HTTP\/1.1<br \/>\nContent-Type: application\/json<br \/>\nIf-Match: &#034;abc123&#034;<\/p>\n<p>{<br \/>\n  &#034;name&#034;: &#034;Updated Name&#034;,<br \/>\n  &#034;version&#034;: 2<br \/>\n}<\/p>\n<p>HTTP\/1.1 409 Conflict<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;VERSION_CONFLICT&#034;,<br \/>\n    &#034;message&#034;: &#034;Resource has been modified by another request&#034;,<br \/>\n    &#034;current_version&#034;: &#034;def456&#034;,<br \/>\n    &#034;suggested_action&#034;: &#034;Retrieve the latest version and retry&#034;<br \/>\n  }<br \/>\n}<\/p>\n<p>422 Unprocessable Entity &#8211; \u8bed\u4e49\u9519\u8bef<\/p>\n<p>http<\/p>\n<p>POST \/orders HTTP\/1.1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;items&#034;: [],<br \/>\n  &#034;shipping_address&#034;: {<br \/>\n    &#034;street&#034;: &#034;123 Main St&#034;<br \/>\n    \/\/ missing city and country<br \/>\n  }<br \/>\n}<\/p>\n<p>HTTP\/1.1 422 Unprocessable Entity<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;UNPROCESSABLE_ENTITY&#034;,<br \/>\n    &#034;message&#034;: &#034;The request was well-formed but contains semantic errors&#034;,<br \/>\n    &#034;details&#034;: [<br \/>\n      {<br \/>\n        &#034;field&#034;: &#034;shipping_address.city&#034;,<br \/>\n        &#034;message&#034;: &#034;City is required&#034;<br \/>\n      },<br \/>\n      {<br \/>\n        &#034;field&#034;: &#034;shipping_address.country&#034;,<br \/>\n        &#034;message&#034;: &#034;Country is required&#034;<br \/>\n      },<br \/>\n      {<br \/>\n        &#034;field&#034;: &#034;items&#034;,<br \/>\n        &#034;message&#034;: &#034;Order must contain at least one item&#034;<br \/>\n      }<br \/>\n    ]<br \/>\n  }<br \/>\n}<\/p>\n<h5>30.2.4 \u670d\u52a1\u5668\u9519\u8bef\u7c7b\u72b6\u6001\u7801&#xff08;5xx&#xff09;<\/h5>\n<p>500 Internal Server Error &#8211; \u901a\u7528\u670d\u52a1\u5668\u9519\u8bef<\/p>\n<p>http<\/p>\n<p>GET \/users HTTP\/1.1<\/p>\n<p>HTTP\/1.1 500 Internal Server Error<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;INTERNAL_SERVER_ERROR&#034;,<br \/>\n    &#034;message&#034;: &#034;An unexpected error occurred&#034;,<br \/>\n    &#034;reference&#034;: &#034;ERR-20240115-001&#034;,<br \/>\n    &#034;support_contact&#034;: &#034;support&#064;api.example.com&#034;<br \/>\n  }<br \/>\n}<\/p>\n<p>502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout<\/p>\n<p>http<\/p>\n<p>GET \/api\/third-party-data HTTP\/1.1<\/p>\n<p>HTTP\/1.1 503 Service Unavailable<br \/>\nRetry-After: 300<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;SERVICE_UNAVAILABLE&#034;,<br \/>\n    &#034;message&#034;: &#034;Service is temporarily unavailable for maintenance&#034;,<br \/>\n    &#034;estimated_recovery&#034;: &#034;2024-01-15T12:00:00Z&#034;,<br \/>\n    &#034;status_page&#034;: &#034;https:\/\/status.example.com&#034;<br \/>\n  }<br \/>\n}<\/p>\n<h4>30.3 \u9ad8\u7ea7\u72b6\u6001\u7801\u4f7f\u7528\u6a21\u5f0f<\/h4>\n<h5>30.3.1 \u6761\u4ef6\u8bf7\u6c42\u4e0e\u72b6\u6001\u7801<\/h5>\n<p>ETag\u4e0eIf-None-Match<\/p>\n<p>http<\/p>\n<p>GET \/articles\/123 HTTP\/1.1<br \/>\nIf-None-Match: &#034;abc123&#034;<\/p>\n<p># \u5982\u679cETag\u5339\u914d<br \/>\nHTTP\/1.1 304 Not Modified<br \/>\nETag: &#034;abc123&#034;<\/p>\n<p># \u5982\u679cETag\u4e0d\u5339\u914d<br \/>\nHTTP\/1.1 200 OK<br \/>\nETag: &#034;def456&#034;<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;id&#034;: &#034;123&#034;,<br \/>\n  &#034;title&#034;: &#034;Updated Article&#034;,<br \/>\n  &#034;content&#034;: &#034;&#8230;&#034;,<br \/>\n  &#034;updated_at&#034;: &#034;2024-01-15T10:00:00Z&#034;<br \/>\n}<\/p>\n<p>Last-Modified\u4e0eIf-Modified-Since<\/p>\n<p>http<\/p>\n<p>GET \/articles HTTP\/1.1<br \/>\nIf-Modified-Since: Mon, 15 Jan 2024 09:00:00 GMT<\/p>\n<p># \u5982\u679c\u6ca1\u6709\u4fee\u6539<br \/>\nHTTP\/1.1 304 Not Modified<br \/>\nLast-Modified: Mon, 15 Jan 2024 08:00:00 GMT<\/p>\n<p># \u5982\u679c\u6709\u4fee\u6539<br \/>\nHTTP\/1.1 200 OK<br \/>\nLast-Modified: Mon, 15 Jan 2024 10:00:00 GMT<br \/>\nContent-Type: application\/json<\/p>\n<p>[<br \/>\n  {<br \/>\n    &#034;id&#034;: &#034;123&#034;,<br \/>\n    &#034;title&#034;: &#034;New Article&#034;,<br \/>\n    &#034;created_at&#034;: &#034;2024-01-15T09:30:00Z&#034;<br \/>\n  }<br \/>\n]<\/p>\n<h5>30.3.2 \u8303\u56f4\u8bf7\u6c42\u4e0e206 Partial Content<\/h5>\n<p>http<\/p>\n<p>GET \/large-file.pdf HTTP\/1.1<br \/>\nRange: bytes&#061;0-999<\/p>\n<p>HTTP\/1.1 206 Partial Content<br \/>\nContent-Range: bytes 0-999\/5000<br \/>\nContent-Length: 1000<br \/>\nContent-Type: application\/pdf<\/p>\n<p>&lt;binary data&gt;<\/p>\n<h5>30.3.3 \u591a\u72b6\u6001\u54cd\u5e94\u4e0e207 Multi-Status<\/h5>\n<p>http<\/p>\n<p>PROPPATCH \/webdav\/resources HTTP\/1.1<br \/>\nContent-Type: application\/xml<\/p>\n<p>&lt;?xml version&#061;&#034;1.0&#034; encoding&#061;&#034;utf-8&#034; ?&gt;<br \/>\n&lt;D:propertyupdate xmlns:D&#061;&#034;DAV:&#034;&gt;<br \/>\n  &lt;D:set&gt;<br \/>\n    &lt;D:prop&gt;<br \/>\n      &lt;D:displayname&gt;New Name&lt;\/D:displayname&gt;<br \/>\n    &lt;\/D:prop&gt;<br \/>\n  &lt;\/D:set&gt;<br \/>\n&lt;\/D:propertyupdate&gt;<\/p>\n<p>HTTP\/1.1 207 Multi-Status<br \/>\nContent-Type: application\/xml<\/p>\n<p>&lt;?xml version&#061;&#034;1.0&#034; encoding&#061;&#034;utf-8&#034; ?&gt;<br \/>\n&lt;D:multistatus xmlns:D&#061;&#034;DAV:&#034;&gt;<br \/>\n  &lt;D:response&gt;<br \/>\n    &lt;D:href&gt;\/webdav\/resource1&lt;\/D:href&gt;<br \/>\n    &lt;D:status&gt;HTTP\/1.1 200 OK&lt;\/D:status&gt;<br \/>\n  &lt;\/D:response&gt;<br \/>\n  &lt;D:response&gt;<br \/>\n    &lt;D:href&gt;\/webdav\/resource2&lt;\/D:href&gt;<br \/>\n    &lt;D:status&gt;HTTP\/1.1 403 Forbidden&lt;\/D:status&gt;<br \/>\n    &lt;D:responsedescription&gt;Permission denied&lt;\/D:responsedescription&gt;<br \/>\n  &lt;\/D:response&gt;<br \/>\n&lt;\/D:multistatus&gt;<\/p>\n<h4>30.4 API\u7248\u672c\u63a7\u5236\u4e0e\u72b6\u6001\u7801<\/h4>\n<h5>30.4.1 \u7248\u672c\u8fc7\u671f\u4e0e410 Gone<\/h5>\n<p>http<\/p>\n<p>GET \/api\/v1\/users HTTP\/1.1<\/p>\n<p>HTTP\/1.1 410 Gone<br \/>\nContent-Type: application\/json<br \/>\nLink: &lt;\/api\/v2\/users&gt;; rel&#061;&#034;latest-version&#034;<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;API_VERSION_DEPRECATED&#034;,<br \/>\n    &#034;message&#034;: &#034;API v1 has been deprecated since 2024-01-01&#034;,<br \/>\n    &#034;deprecation_date&#034;: &#034;2024-01-01T00:00:00Z&#034;,<br \/>\n    &#034;sunset_date&#034;: &#034;2024-07-01T00:00:00Z&#034;,<br \/>\n    &#034;migration_guide&#034;: &#034;https:\/\/docs.example.com\/migration\/v1-to-v2&#034;<br \/>\n  }<br \/>\n}<\/p>\n<h5>30.4.2 \u7248\u672c\u91cd\u5b9a\u5411\u4e0e301\/308<\/h5>\n<p>http<\/p>\n<p>GET \/api\/v1\/products HTTP\/1.1<\/p>\n<p>HTTP\/1.1 308 Permanent Redirect<br \/>\nLocation: \/api\/v2\/products<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;message&#034;: &#034;API endpoint has been permanently moved&#034;,<br \/>\n  &#034;deprecation_notice&#034;: &#034;v1 will be sunset on 2024-06-30&#034;<br \/>\n}<\/p>\n<h4>30.5 \u9519\u8bef\u54cd\u5e94\u6807\u51c6\u5316<\/h4>\n<h5>30.5.1 RFC 7807\u95ee\u9898\u8be6\u60c5\u683c\u5f0f<\/h5>\n<p>http<\/p>\n<p>POST \/transfer HTTP\/1.1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;from_account&#034;: &#034;acc123&#034;,<br \/>\n  &#034;to_account&#034;: &#034;acc456&#034;,<br \/>\n  &#034;amount&#034;: 1000<br \/>\n}<\/p>\n<p>HTTP\/1.1 422 Unprocessable Entity<br \/>\nContent-Type: application\/problem&#043;json<\/p>\n<p>{<br \/>\n  &#034;type&#034;: &#034;https:\/\/api.example.com\/errors\/insufficient-funds&#034;,<br \/>\n  &#034;title&#034;: &#034;Insufficient Funds&#034;,<br \/>\n  &#034;status&#034;: 422,<br \/>\n  &#034;detail&#034;: &#034;Account acc123 has only 500.00 available, but 1000.00 was requested&#034;,<br \/>\n  &#034;instance&#034;: &#034;\/transfer\/attempt\/abc-123-def-456&#034;,<br \/>\n  &#034;balance&#034;: 500.00,<br \/>\n  &#034;accounts&#034;: [&#034;acc123&#034;, &#034;acc456&#034;],<br \/>\n  &#034;extensions&#034;: {<br \/>\n    &#034;code&#034;: &#034;INSUFFICIENT_FUNDS&#034;,<br \/>\n    &#034;request_id&#034;: &#034;req-123456&#034;,<br \/>\n    &#034;timestamp&#034;: &#034;2024-01-15T10:30:00Z&#034;<br \/>\n  }<br \/>\n}<\/p>\n<h5>30.5.2 Google API\u9519\u8bef\u683c\u5f0f<\/h5>\n<p>http<\/p>\n<p>GET \/v1\/users\/invalid-id HTTP\/1.1<\/p>\n<p>HTTP\/1.1 404 Not Found<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: 404,<br \/>\n    &#034;message&#034;: &#034;User not found: invalid-id&#034;,<br \/>\n    &#034;status&#034;: &#034;NOT_FOUND&#034;,<br \/>\n    &#034;details&#034;: [<br \/>\n      {<br \/>\n        &#034;&#064;type&#034;: &#034;type.googleapis.com\/google.rpc.ErrorInfo&#034;,<br \/>\n        &#034;reason&#034;: &#034;USER_NOT_FOUND&#034;,<br \/>\n        &#034;domain&#034;: &#034;users.googleapis.com&#034;,<br \/>\n        &#034;metadata&#034;: {<br \/>\n          &#034;resource&#034;: &#034;users\/invalid-id&#034;,<br \/>\n          &#034;service&#034;: &#034;user-service&#034;<br \/>\n        }<br \/>\n      },<br \/>\n      {<br \/>\n        &#034;&#064;type&#034;: &#034;type.googleapis.com\/google.rpc.LocalizedMessage&#034;,<br \/>\n        &#034;locale&#034;: &#034;en-US&#034;,<br \/>\n        &#034;message&#034;: &#034;The requested user does not exist.&#034;<br \/>\n      }<br \/>\n    ]<br \/>\n  }<br \/>\n}<\/p>\n<h4>30.6 \u76d1\u63a7\u4e0e\u53ef\u89c2\u6d4b\u6027<\/h4>\n<h5>30.6.1 \u72b6\u6001\u7801\u5206\u5e03\u76d1\u63a7<\/h5>\n<p>prometheus<\/p>\n<p># Prometheus\u6307\u6807\u793a\u4f8b<br \/>\nhttp_requests_total{method&#061;&#034;POST&#034;, endpoint&#061;&#034;\/users&#034;, status&#061;&#034;201&#034;} 153<br \/>\nhttp_requests_total{method&#061;&#034;POST&#034;, endpoint&#061;&#034;\/users&#034;, status&#061;&#034;400&#034;} 12<br \/>\nhttp_requests_total{method&#061;&#034;GET&#034;, endpoint&#061;&#034;\/users\/{id}&#034;, status&#061;&#034;404&#034;} 8<br \/>\nhttp_requests_total{method&#061;&#034;GET&#034;, endpoint&#061;&#034;\/users&#034;, status&#061;&#034;500&#034;} 3<\/p>\n<p># \u9519\u8bef\u7387\u8ba1\u7b97<br \/>\nrate(http_requests_total{status&#061;~&#034;4..|5..&#034;}[5m]) \/ rate(http_requests_total[5m])<\/p>\n<h5>30.6.2 \u7ed3\u6784\u5316\u65e5\u5fd7\u4e2d\u7684\u72b6\u6001\u7801<\/h5>\n<p>json<\/p>\n<p>{<br \/>\n  &#034;timestamp&#034;: &#034;2024-01-15T10:30:15.123Z&#034;,<br \/>\n  &#034;level&#034;: &#034;INFO&#034;,<br \/>\n  &#034;message&#034;: &#034;HTTP request processed&#034;,<br \/>\n  &#034;http&#034;: {<br \/>\n    &#034;method&#034;: &#034;POST&#034;,<br \/>\n    &#034;path&#034;: &#034;\/users&#034;,<br \/>\n    &#034;status_code&#034;: 201,<br \/>\n    &#034;duration_ms&#034;: 45<br \/>\n  },<br \/>\n  &#034;user&#034;: {<br \/>\n    &#034;id&#034;: &#034;user-123&#034;,<br \/>\n    &#034;role&#034;: &#034;admin&#034;<br \/>\n  },<br \/>\n  &#034;request_id&#034;: &#034;req-abc-123&#034;,<br \/>\n  &#034;correlation_id&#034;: &#034;corr-xyz-789&#034;,<br \/>\n  &#034;tags&#034;: [&#034;api&#034;, &#034;user-service&#034;]<br \/>\n}<\/p>\n<h4>30.7 \u5b89\u5168\u8003\u8651<\/h4>\n<h5>30.7.1 \u4fe1\u606f\u6cc4\u9732\u9884\u9632<\/h5>\n<p>\u4e0d\u5f53\u793a\u4f8b&#xff08;\u6cc4\u9732\u5185\u90e8\u4fe1\u606f&#xff09;&#xff1a;<\/p>\n<p>http<\/p>\n<p>HTTP\/1.1 500 Internal Server Error<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;message&#034;: &#034;Database connection failed: Connection refused to 10.0.0.5:5432&#034;,<br \/>\n    &#034;stack_trace&#034;: &#034;java.sql.SQLException: Connection refused&#8230;&#034;,<br \/>\n    &#034;internal_code&#034;: &#034;DB_CONN_REFUSED&#034;,<br \/>\n    &#034;database_ip&#034;: &#034;10.0.0.5&#034;<br \/>\n  }<br \/>\n}<\/p>\n<p>\u6b63\u786e\u793a\u4f8b&#xff1a;<\/p>\n<p>http<\/p>\n<p>HTTP\/1.1 500 Internal Server Error<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;INTERNAL_ERROR&#034;,<br \/>\n    &#034;message&#034;: &#034;An internal server error occurred&#034;,<br \/>\n    &#034;reference&#034;: &#034;ERR-20240115-001&#034;,<br \/>\n    &#034;support_contact&#034;: &#034;support&#064;example.com&#034;<br \/>\n  }<br \/>\n}<\/p>\n<h5>30.7.2 \u5b89\u5168\u5934\u4e0e\u72b6\u6001\u7801<\/h5>\n<p>http<\/p>\n<p>HTTP\/1.1 400 Bad Request<br \/>\nContent-Type: application\/json<br \/>\nStrict-Transport-Security: max-age&#061;31536000; includeSubDomains<br \/>\nX-Content-Type-Options: nosniff<br \/>\nX-Frame-Options: DENY<br \/>\nContent-Security-Policy: default-src &#039;self&#039;<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;VALIDATION_ERROR&#034;,<br \/>\n    &#034;message&#034;: &#034;Invalid input provided&#034;<br \/>\n  }<br \/>\n}<\/p>\n<h4>30.8 \u5b9e\u9645\u6848\u4f8b\u5206\u6790<\/h4>\n<h5>30.8.1 GitHub REST API\u72b6\u6001\u7801\u4f7f\u7528<\/h5>\n<p>http<\/p>\n<p># \u521b\u5efa\u4ed3\u5e93<br \/>\nPOST \/user\/repos HTTP\/1.1<br \/>\nAuthorization: token ghp_abc123<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;name&#034;: &#034;new-repo&#034;,<br \/>\n  &#034;description&#034;: &#034;A new repository&#034;<br \/>\n}<\/p>\n<p># \u6210\u529f\u54cd\u5e94<br \/>\nHTTP\/1.1 201 Created<br \/>\nLocation: https:\/\/api.github.com\/repos\/octocat\/new-repo<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;id&#034;: 1296269,<br \/>\n  &#034;name&#034;: &#034;new-repo&#034;,<br \/>\n  &#034;full_name&#034;: &#034;octocat\/new-repo&#034;,<br \/>\n  &#034;private&#034;: false,<br \/>\n  &#034;html_url&#034;: &#034;https:\/\/github.com\/octocat\/new-repo&#034;,<br \/>\n  &#034;created_at&#034;: &#034;2011-01-26T19:01:12Z&#034;<br \/>\n}<\/p>\n<p># \u51b2\u7a81\u54cd\u5e94&#xff08;\u4ed3\u5e93\u5df2\u5b58\u5728&#xff09;<br \/>\nHTTP\/1.1 422 Unprocessable Entity<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;message&#034;: &#034;Repository creation failed.&#034;,<br \/>\n  &#034;errors&#034;: [<br \/>\n    {<br \/>\n      &#034;resource&#034;: &#034;Repository&#034;,<br \/>\n      &#034;code&#034;: &#034;custom&#034;,<br \/>\n      &#034;field&#034;: &#034;name&#034;,<br \/>\n      &#034;message&#034;: &#034;name already exists on this account&#034;<br \/>\n    }<br \/>\n  ],<br \/>\n  &#034;documentation_url&#034;: &#034;https:\/\/docs.github.com\/rest\/repos\/repos#create-a-repository-for-the-authenticated-user&#034;<br \/>\n}<\/p>\n<h5>30.8.2 Stripe API\u72b6\u6001\u7801\u4f7f\u7528<\/h5>\n<p>http<\/p>\n<p># \u521b\u5efa\u652f\u4ed8\u610f\u56fe<br \/>\nPOST \/v1\/payment_intents HTTP\/1.1<br \/>\nAuthorization: Bearer sk_test_abc123<br \/>\nContent-Type: application\/x-www-form-urlencoded<\/p>\n<p>amount&#061;2000&amp;currency&#061;usd&amp;payment_method_types[]&#061;card<\/p>\n<p># \u6210\u529f\u54cd\u5e94<br \/>\nHTTP\/1.1 200 OK<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;id&#034;: &#034;pi_1JABC123&#034;,<br \/>\n  &#034;object&#034;: &#034;payment_intent&#034;,<br \/>\n  &#034;amount&#034;: 2000,<br \/>\n  &#034;currency&#034;: &#034;usd&#034;,<br \/>\n  &#034;status&#034;: &#034;requires_payment_method&#034;,<br \/>\n  &#034;client_secret&#034;: &#034;pi_1JABC123_secret_xyz789&#034;<br \/>\n}<\/p>\n<p># \u65e0\u6548\u53c2\u6570\u54cd\u5e94<br \/>\nHTTP\/1.1 400 Bad Request<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;type&#034;: &#034;invalid_request_error&#034;,<br \/>\n    &#034;message&#034;: &#034;Invalid integer: abc&#034;,<br \/>\n    &#034;param&#034;: &#034;amount&#034;<br \/>\n  }<br \/>\n}<\/p>\n<h4>30.9 \u6700\u4f73\u5b9e\u8df5\u603b\u7ed3<\/h4>\n<li>\n<p>\u4e00\u81f4\u6027\u662f\u5173\u952e&#xff1a;\u5728\u6574\u4e2aAPI\u4e2d\u4fdd\u6301\u72b6\u6001\u7801\u4f7f\u7528\u7684\u4e00\u81f4\u6027<\/p>\n<\/li>\n<li>\n<p>\u6b63\u786e\u7684\u72b6\u6001\u7801&#xff1a;\u4e3a\u6bcf\u79cd\u60c5\u51b5\u9009\u62e9\u8bed\u4e49\u6700\u51c6\u786e\u7684\u72b6\u6001\u7801<\/p>\n<\/li>\n<li>\n<p>\u4e30\u5bcc\u7684\u9519\u8bef\u4fe1\u606f&#xff1a;\u63d0\u4f9b\u53ef\u64cd\u4f5c\u7684\u9519\u8bef\u8be6\u60c5&#xff0c;\u4f46\u907f\u514d\u4fe1\u606f\u6cc4\u9732<\/p>\n<\/li>\n<li>\n<p>\u9002\u5f53\u7684\u91cd\u8bd5\u6307\u5bfc&#xff1a;\u901a\u8fc7Retry-After\u5934\u6216\u9519\u8bef\u4fe1\u606f\u6307\u5bfc\u5ba2\u6237\u7aef\u91cd\u8bd5<\/p>\n<\/li>\n<li>\n<p>\u76d1\u63a7\u4e0e\u544a\u8b66&#xff1a;\u57fa\u4e8e\u72b6\u6001\u7801\u5206\u5e03\u8bbe\u7f6e\u76d1\u63a7\u548c\u544a\u8b66<\/p>\n<\/li>\n<li>\n<p>\u7248\u672c\u63a7\u5236&#xff1a;\u660e\u786e\u5904\u7406API\u7248\u672c\u53d8\u66f4\u548c\u5e9f\u5f03<\/p>\n<\/li>\n<li>\n<p>\u5b89\u5168\u8003\u8651&#xff1a;\u5728\u9519\u8bef\u54cd\u5e94\u4e2d\u5b9e\u65bd\u9002\u5f53\u7684\u5b89\u5168\u63aa\u65bd<\/p>\n<\/li>\n<li>\n<p>\u6587\u6863\u5316&#xff1a;\u5b8c\u6574\u8bb0\u5f55\u6bcf\u4e2a\u7aef\u70b9\u53ef\u80fd\u8fd4\u56de\u7684\u72b6\u6001\u7801\u53ca\u5176\u542b\u4e49<\/p>\n<\/li>\n<h4>30.10 \u5e38\u89c1\u9677\u9631\u4e0e\u89e3\u51b3\u65b9\u6848<\/h4>\n<h5>30.10.1 \u9677\u9631&#xff1a;\u603b\u662f\u8fd4\u56de200 OK<\/h5>\n<p>\u9519\u8bef\u793a\u4f8b&#xff1a;<\/p>\n<p>http<\/p>\n<p>POST \/login HTTP\/1.1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;username&#034;: &#034;wrong&#034;,<br \/>\n  &#034;password&#034;: &#034;wrong&#034;<br \/>\n}<\/p>\n<p>HTTP\/1.1 200 OK<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;success&#034;: false,<br \/>\n  &#034;error&#034;: &#034;Invalid credentials&#034;<br \/>\n}<\/p>\n<p>\u6b63\u786e\u505a\u6cd5&#xff1a;<\/p>\n<p>http<\/p>\n<p>HTTP\/1.1 401 Unauthorized<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;INVALID_CREDENTIALS&#034;,<br \/>\n    &#034;message&#034;: &#034;Username or password is incorrect&#034;<br \/>\n  }<br \/>\n}<\/p>\n<h5>30.10.2 \u9677\u9631&#xff1a;\u8fc7\u5ea6\u4f7f\u7528500\u9519\u8bef<\/h5>\n<p>\u9519\u8bef\u793a\u4f8b&#xff1a;<\/p>\n<p>http<\/p>\n<p>GET \/users\/999 HTTP\/1.1<\/p>\n<p>HTTP\/1.1 500 Internal Server Error<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: &#034;User not found&#034;<br \/>\n}<\/p>\n<p>\u6b63\u786e\u505a\u6cd5&#xff1a;<\/p>\n<p>http<\/p>\n<p>HTTP\/1.1 404 Not Found<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#034;error&#034;: {<br \/>\n    &#034;code&#034;: &#034;USER_NOT_FOUND&#034;,<br \/>\n    &#034;message&#034;: &#034;User with id 999 does not exist&#034;<br \/>\n  }<br \/>\n}<\/p>\n<hr \/>\n<h3>\u7b2c31\u7ae0&#xff1a;Web\u5f00\u53d1\u4e2d\u7684\u72b6\u6001\u7801\u6700\u4f73\u5b9e\u8df5<\/h3>\n<h4>31.1 \u73b0\u4ee3Web\u67b6\u6784\u4e2d\u7684\u72b6\u6001\u7801<\/h4>\n<h5>31.1.1 \u5355\u9875\u5e94\u7528&#xff08;SPA&#xff09;\u4e0e\u72b6\u6001\u7801<\/h5>\n<p>\u5728\u73b0\u4ee3\u5355\u9875\u5e94\u7528\u4e2d&#xff0c;\u72b6\u6001\u7801\u7684\u5904\u7406\u5448\u73b0\u53cc\u91cd\u6027&#xff1a;\u5ba2\u6237\u7aef\u8def\u7531\u548cAPI\u901a\u4fe1\u3002<\/p>\n<p>\u5ba2\u6237\u7aef\u8def\u7531\u72b6\u6001\u7801\u6a21\u62df<\/p>\n<p>javascript<\/p>\n<p>\/\/ React Router v6\u4e2d\u7684\u72b6\u6001\u7801\u5904\u7406<br \/>\nimport { useRouteError, isRouteErrorResponse } from &#039;react-router-dom&#039;;<\/p>\n<p>function ErrorBoundary() {<br \/>\n  const error &#061; useRouteError();<\/p>\n<p>  if (isRouteErrorResponse(error)) {<br \/>\n    return (<br \/>\n      &lt;div className&#061;&#034;error-page&#034;&gt;<br \/>\n        &lt;h1&gt;{error.status} {error.statusText}&lt;\/h1&gt;<br \/>\n        {error.status &#061;&#061;&#061; 404 &amp;&amp; (<br \/>\n          &lt;div&gt;<br \/>\n            &lt;h2&gt;\u9875\u9762\u672a\u627e\u5230&lt;\/h2&gt;<br \/>\n            &lt;p&gt;\u60a8\u8bbf\u95ee\u7684\u9875\u9762\u4e0d\u5b58\u5728\u6216\u5df2\u88ab\u79fb\u52a8&lt;\/p&gt;<br \/>\n            &lt;Link to&#061;&#034;\/&#034;&gt;\u8fd4\u56de\u9996\u9875&lt;\/Link&gt;<br \/>\n          &lt;\/div&gt;<br \/>\n        )}<br \/>\n        {error.status &#061;&#061;&#061; 401 &amp;&amp; (<br \/>\n          &lt;div&gt;<br \/>\n            &lt;h2&gt;\u9700\u8981\u767b\u5f55&lt;\/h2&gt;<br \/>\n            &lt;p&gt;\u8bf7\u767b\u5f55\u540e\u8bbf\u95ee\u6b64\u9875\u9762&lt;\/p&gt;<br \/>\n            &lt;button onClick&#061;{() &#061;&gt; navigate(&#039;\/login&#039;)}&gt;\u524d\u5f80\u767b\u5f55&lt;\/button&gt;<br \/>\n          &lt;\/div&gt;<br \/>\n        )}<br \/>\n      &lt;\/div&gt;<br \/>\n    );<br \/>\n  }<\/p>\n<p>  return &lt;div&gt;\u53d1\u751f\u4e86\u672a\u77e5\u9519\u8bef&lt;\/div&gt;;<br \/>\n}<\/p>\n<p>API\u8bf7\u6c42\u72b6\u6001\u7801\u7edf\u4e00\u5904\u7406<\/p>\n<p>javascript<\/p>\n<p>\/\/ Axios\u62e6\u622a\u5668\u914d\u7f6e<br \/>\nconst apiClient &#061; axios.create({<br \/>\n  baseURL: process.env.REACT_APP_API_URL,<br \/>\n  timeout: 10000,<br \/>\n});<\/p>\n<p>\/\/ \u8bf7\u6c42\u62e6\u622a\u5668<br \/>\napiClient.interceptors.request.use(<br \/>\n  config &#061;&gt; {<br \/>\n    const token &#061; localStorage.getItem(&#039;access_token&#039;);<br \/>\n    if (token) {<br \/>\n      config.headers.Authorization &#061; &#096;Bearer ${token}&#096;;<br \/>\n    }<br \/>\n    return config;<br \/>\n  },<br \/>\n  error &#061;&gt; Promise.reject(error)<br \/>\n);<\/p>\n<p>\/\/ \u54cd\u5e94\u62e6\u622a\u5668<br \/>\napiClient.interceptors.response.use(<br \/>\n  response &#061;&gt; response,<br \/>\n  error &#061;&gt; {<br \/>\n    if (!error.response) {<br \/>\n      \/\/ \u7f51\u7edc\u9519\u8bef<br \/>\n      return Promise.reject({<br \/>\n        code: &#039;NETWORK_ERROR&#039;,<br \/>\n        message: &#039;\u7f51\u7edc\u8fde\u63a5\u5931\u8d25&#xff0c;\u8bf7\u68c0\u67e5\u7f51\u7edc\u8bbe\u7f6e&#039;,<br \/>\n        originalError: error<br \/>\n      });<br \/>\n    }<\/p>\n<p>    const { status, data } &#061; error.response;<\/p>\n<p>    switch (status) {<br \/>\n      case 400:<br \/>\n        \/\/ \u7edf\u4e00\u5904\u7406\u9a8c\u8bc1\u9519\u8bef<br \/>\n        return Promise.reject({<br \/>\n          code: &#039;VALIDATION_ERROR&#039;,<br \/>\n          message: data.error?.message || &#039;\u8bf7\u6c42\u53c2\u6570\u65e0\u6548&#039;,<br \/>\n          details: data.error?.details,<br \/>\n          originalError: error<br \/>\n        });<\/p>\n<p>      case 401:<br \/>\n        \/\/ Token\u8fc7\u671f\u5904\u7406<br \/>\n        if (data.error?.code &#061;&#061;&#061; &#039;TOKEN_EXPIRED&#039;) {<br \/>\n          return handleTokenRefresh(error);<br \/>\n        }<br \/>\n        \/\/ \u5f3a\u5236\u767b\u51fa<br \/>\n        localStorage.removeItem(&#039;access_token&#039;);<br \/>\n        localStorage.removeItem(&#039;refresh_token&#039;);<br \/>\n        window.location.href &#061; &#039;\/login?redirect&#061;&#039; &#043; encodeURIComponent(window.location.pathname);<br \/>\n        return Promise.reject({<br \/>\n          code: &#039;UNAUTHORIZED&#039;,<br \/>\n          message: &#039;\u767b\u5f55\u5df2\u8fc7\u671f&#xff0c;\u8bf7\u91cd\u65b0\u767b\u5f55&#039;,<br \/>\n          originalError: error<br \/>\n        });<\/p>\n<p>      case 403:<br \/>\n        \/\/ \u6743\u9650\u4e0d\u8db3<br \/>\n        return Promise.reject({<br \/>\n          code: &#039;FORBIDDEN&#039;,<br \/>\n          message: &#039;\u60a8\u6ca1\u6709\u6743\u9650\u6267\u884c\u6b64\u64cd\u4f5c&#039;,<br \/>\n          originalError: error<br \/>\n        });<\/p>\n<p>      case 404:<br \/>\n        return Promise.reject({<br \/>\n          code: &#039;NOT_FOUND&#039;,<br \/>\n          message: data.error?.message || &#039;\u8bf7\u6c42\u7684\u8d44\u6e90\u4e0d\u5b58\u5728&#039;,<br \/>\n          originalError: error<br \/>\n        });<\/p>\n<p>      case 429:<br \/>\n        \/\/ \u8bf7\u6c42\u9891\u7387\u9650\u5236<br \/>\n        const retryAfter &#061; error.response.headers[&#039;retry-after&#039;];<br \/>\n        return Promise.reject({<br \/>\n          code: &#039;RATE_LIMITED&#039;,<br \/>\n          message: &#039;\u8bf7\u6c42\u8fc7\u4e8e\u9891\u7e41&#xff0c;\u8bf7\u7a0d\u540e\u91cd\u8bd5&#039;,<br \/>\n          retryAfter,<br \/>\n          originalError: error<br \/>\n        });<\/p>\n<p>      case 500:<br \/>\n      case 502:<br \/>\n      case 503:<br \/>\n      case 504:<br \/>\n        \/\/ \u670d\u52a1\u5668\u9519\u8bef<br \/>\n        return Promise.reject({<br \/>\n          code: &#039;SERVER_ERROR&#039;,<br \/>\n          message: &#039;\u670d\u52a1\u5668\u6682\u65f6\u4e0d\u53ef\u7528&#xff0c;\u8bf7\u7a0d\u540e\u91cd\u8bd5&#039;,<br \/>\n          status,<br \/>\n          originalError: error<br \/>\n        });<\/p>\n<p>      default:<br \/>\n        return Promise.reject({<br \/>\n          code: &#039;UNKNOWN_ERROR&#039;,<br \/>\n          message: &#096;\u672a\u77e5\u9519\u8bef (${status})&#096;,<br \/>\n          originalError: error<br \/>\n        });<br \/>\n    }<br \/>\n  }<br \/>\n);<\/p>\n<h5>31.1.2 \u670d\u52a1\u7aef\u6e32\u67d3&#xff08;SSR&#xff09;\u4e2d\u7684\u72b6\u6001\u7801<\/h5>\n<p>Next.js\u4e2d\u7684\u72b6\u6001\u7801\u5904\u7406<\/p>\n<p>javascript<\/p>\n<p>\/\/ pages\/404.js &#8211; \u81ea\u5b9a\u4e49404\u9875\u9762<br \/>\nexport default function Custom404() {<br \/>\n  return (<br \/>\n    &lt;div className&#061;&#034;container&#034;&gt;<br \/>\n      &lt;h1&gt;404 &#8211; \u9875\u9762\u672a\u627e\u5230&lt;\/h1&gt;<br \/>\n      &lt;p&gt;\u62b1\u6b49&#xff0c;\u60a8\u8bbf\u95ee\u7684\u9875\u9762\u4e0d\u5b58\u5728&lt;\/p&gt;<br \/>\n      &lt;Link href&#061;&#034;\/&#034;&gt;<br \/>\n        &lt;a&gt;\u8fd4\u56de\u9996\u9875&lt;\/a&gt;<br \/>\n      &lt;\/Link&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n  );<br \/>\n}<\/p>\n<p>\/\/ \u9875\u9762\u7ea7\u522b\u7684\u9519\u8bef\u5904\u7406<br \/>\nexport async function getServerSideProps(context) {<br \/>\n  try {<br \/>\n    const { id } &#061; context.params;<br \/>\n    const res &#061; await fetch(&#096;https:\/\/api.example.com\/products\/${id}&#096;);<\/p>\n<p>    if (res.status &#061;&#061;&#061; 404) {<br \/>\n      return {<br \/>\n        notFound: true, \/\/ \u89e6\u53d1404\u9875\u9762<br \/>\n      };<br \/>\n    }<\/p>\n<p>    if (!res.ok) {<br \/>\n      throw new Error(&#096;Failed to fetch product: ${res.status}&#096;);<br \/>\n    }<\/p>\n<p>    const product &#061; await res.json();<\/p>\n<p>    return {<br \/>\n      props: { product },<br \/>\n    };<br \/>\n  } catch (error) {<br \/>\n    \/\/ \u8bb0\u5f55\u9519\u8bef\u5230\u65e5\u5fd7\u7cfb\u7edf<br \/>\n    console.error(&#039;Product page error:&#039;, error);<\/p>\n<p>    \/\/ \u8fd4\u56de500\u9519\u8bef\u9875\u9762<br \/>\n    return {<br \/>\n      props: {<br \/>\n        error: {<br \/>\n          message: &#039;\u52a0\u8f7d\u4ea7\u54c1\u4fe1\u606f\u5931\u8d25&#039;,<br \/>\n          statusCode: 500<br \/>\n        }<br \/>\n      }<br \/>\n    };<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ \u81ea\u5b9a\u4e49\u9519\u8bef\u9875\u9762<br \/>\nfunction ProductPage({ product, error }) {<br \/>\n  if (error) {<br \/>\n    return (<br \/>\n      &lt;ErrorPage<br \/>\n        statusCode&#061;{error.statusCode}<br \/>\n        message&#061;{error.message}<br \/>\n      \/&gt;<br \/>\n    );<br \/>\n  }<\/p>\n<p>  return (<br \/>\n    &lt;div&gt;<br \/>\n      &lt;h1&gt;{product.name}&lt;\/h1&gt;<br \/>\n      {\/* \u4ea7\u54c1\u8be6\u60c5 *\/}<br \/>\n    &lt;\/div&gt;<br \/>\n  );<br \/>\n}<\/p>\n<h4>31.2 \u72b6\u6001\u7801\u4e0e\u7528\u6237\u4f53\u9a8c<\/h4>\n<h5>31.2.1 \u53cb\u597d\u7684\u9519\u8bef\u9875\u9762\u8bbe\u8ba1<\/h5>\n<p>html<\/p>\n<p>&lt;!&#8211; 404\u9519\u8bef\u9875\u9762\u793a\u4f8b &#8211;&gt;<br \/>\n&lt;!DOCTYPE html&gt;<br \/>\n&lt;html lang&#061;&#034;zh-CN&#034;&gt;<br \/>\n&lt;head&gt;<br \/>\n    &lt;meta charset&#061;&#034;UTF-8&#034;&gt;<br \/>\n    &lt;meta name&#061;&#034;viewport&#034; content&#061;&#034;width&#061;device-width, initial-scale&#061;1.0&#034;&gt;<br \/>\n    &lt;title&gt;404 &#8211; \u9875\u9762\u672a\u627e\u5230&lt;\/title&gt;<br \/>\n    &lt;style&gt;<br \/>\n        .error-container {<br \/>\n            max-width: 600px;<br \/>\n            margin: 100px auto;<br \/>\n            padding: 40px;<br \/>\n            text-align: center;<br \/>\n            font-family: -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;<br \/>\n        }<\/p>\n<p>        .error-code {<br \/>\n            font-size: 120px;<br \/>\n            font-weight: 300;<br \/>\n            color: #e0e0e0;<br \/>\n            margin: 0;<br \/>\n        }<\/p>\n<p>        .error-message {<br \/>\n            font-size: 24px;<br \/>\n            margin: 20px 0;<br \/>\n            color: #333;<br \/>\n        }<\/p>\n<p>        .error-description {<br \/>\n            color: #666;<br \/>\n            margin-bottom: 30px;<br \/>\n            line-height: 1.6;<br \/>\n        }<\/p>\n<p>        .error-actions {<br \/>\n            display: flex;<br \/>\n            gap: 15px;<br \/>\n            justify-content: center;<br \/>\n            flex-wrap: wrap;<br \/>\n        }<\/p>\n<p>        .btn {<br \/>\n            padding: 12px 24px;<br \/>\n            border-radius: 6px;<br \/>\n            text-decoration: none;<br \/>\n            font-weight: 500;<br \/>\n            transition: all 0.2s;<br \/>\n        }<\/p>\n<p>        .btn-primary {<br \/>\n            background: #0070f3;<br \/>\n            color: white;<br \/>\n        }<\/p>\n<p>        .btn-secondary {<br \/>\n            background: #f5f5f5;<br \/>\n            color: #333;<br \/>\n            border: 1px solid #ddd;<br \/>\n        }<\/p>\n<p>        .btn:hover {<br \/>\n            transform: translateY(-2px);<br \/>\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);<br \/>\n        }<\/p>\n<p>        .search-form {<br \/>\n            margin-top: 30px;<br \/>\n            display: flex;<br \/>\n            gap: 10px;<br \/>\n        }<\/p>\n<p>        .search-input {<br \/>\n            flex: 1;<br \/>\n            padding: 12px;<br \/>\n            border: 1px solid #ddd;<br \/>\n            border-radius: 6px;<br \/>\n            font-size: 16px;<br \/>\n        }<\/p>\n<p>        &#064;media (max-width: 600px) {<br \/>\n            .error-container {<br \/>\n                margin: 50px 20px;<br \/>\n                padding: 20px;<br \/>\n            }<\/p>\n<p>            .error-code {<br \/>\n                font-size: 80px;<br \/>\n            }<br \/>\n        }<br \/>\n    &lt;\/style&gt;<br \/>\n&lt;\/head&gt;<br \/>\n&lt;body&gt;<br \/>\n    &lt;div class&#061;&#034;error-container&#034;&gt;<br \/>\n        &lt;h1 class&#061;&#034;error-code&#034;&gt;404&lt;\/h1&gt;<br \/>\n        &lt;h2 class&#061;&#034;error-message&#034;&gt;\u9875\u9762\u8d70\u4e22\u4e86&lt;\/h2&gt;<br \/>\n        &lt;p class&#061;&#034;error-description&#034;&gt;<br \/>\n            \u62b1\u6b49&#xff0c;\u60a8\u8bbf\u95ee\u7684\u9875\u9762\u53ef\u80fd\u5df2\u88ab\u79fb\u9664\u3001\u91cd\u547d\u540d\u6216\u6682\u65f6\u4e0d\u53ef\u7528\u3002<br \/>\n            \u8bf7\u68c0\u67e5\u7f51\u5740\u662f\u5426\u6b63\u786e&#xff0c;\u6216\u4f7f\u7528\u641c\u7d22\u529f\u80fd\u67e5\u627e\u76f8\u5173\u5185\u5bb9\u3002<br \/>\n        &lt;\/p&gt;<\/p>\n<p>        &lt;div class&#061;&#034;error-actions&#034;&gt;<br \/>\n            &lt;a href&#061;&#034;\/&#034; class&#061;&#034;btn btn-primary&#034;&gt;\u8fd4\u56de\u9996\u9875&lt;\/a&gt;<br \/>\n            &lt;a href&#061;&#034;javascript:history.back()&#034; class&#061;&#034;btn btn-secondary&#034;&gt;\u8fd4\u56de\u4e0a\u4e00\u9875&lt;\/a&gt;<br \/>\n            &lt;a href&#061;&#034;\/contact&#034; class&#061;&#034;btn btn-secondary&#034;&gt;\u8054\u7cfb\u652f\u6301&lt;\/a&gt;<br \/>\n        &lt;\/div&gt;<\/p>\n<p>        &lt;form class&#061;&#034;search-form&#034; action&#061;&#034;\/search&#034; method&#061;&#034;GET&#034;&gt;<br \/>\n            &lt;input<br \/>\n                type&#061;&#034;text&#034;<br \/>\n                class&#061;&#034;search-input&#034;<br \/>\n                name&#061;&#034;q&#034;<br \/>\n                placeholder&#061;&#034;\u641c\u7d22\u60a8\u9700\u8981\u7684\u5185\u5bb9&#8230;&#034;<br \/>\n                autofocus<br \/>\n            &gt;<br \/>\n            &lt;button type&#061;&#034;submit&#034; class&#061;&#034;btn btn-primary&#034;&gt;\u641c\u7d22&lt;\/button&gt;<br \/>\n        &lt;\/form&gt;<\/p>\n<p>        &lt;!&#8211; \u70ed\u95e8\u94fe\u63a5\u63a8\u8350 &#8211;&gt;<br \/>\n        &lt;div style&#061;&#034;margin-top: 40px; text-align: left;&#034;&gt;<br \/>\n            &lt;h3 style&#061;&#034;margin-bottom: 15px;&#034;&gt;\u70ed\u95e8\u9875\u9762&lt;\/h3&gt;<br \/>\n            &lt;ul style&#061;&#034;list-style: none; padding: 0; margin: 0;&#034;&gt;<br \/>\n                &lt;li style&#061;&#034;margin-bottom: 10px;&#034;&gt;<br \/>\n                    &lt;a href&#061;&#034;\/products&#034; style&#061;&#034;color: #0070f3; text-decoration: none;&#034;&gt;<br \/>\n                        \u4ea7\u54c1\u4e2d\u5fc3<br \/>\n                    &lt;\/a&gt;<br \/>\n                &lt;\/li&gt;<br \/>\n                &lt;li style&#061;&#034;margin-bottom: 10px;&#034;&gt;<br \/>\n                    &lt;a href&#061;&#034;\/docs&#034; style&#061;&#034;color: #0070f3; text-decoration: none;&#034;&gt;<br \/>\n                        \u6587\u6863\u4e2d\u5fc3<br \/>\n                    &lt;\/a&gt;<br \/>\n                &lt;\/li&gt;<br \/>\n                &lt;li style&#061;&#034;margin-bottom: 10px;&#034;&gt;<br \/>\n                    &lt;a href&#061;&#034;\/blog&#034; style&#061;&#034;color: #0070f3; text-decoration: none;&#034;&gt;<br \/>\n                        \u6280\u672f\u535a\u5ba2<br \/>\n                    &lt;\/a&gt;<br \/>\n                &lt;\/li&gt;<br \/>\n            &lt;\/ul&gt;<br \/>\n        &lt;\/div&gt;<br \/>\n    &lt;\/div&gt;<br \/>\n&lt;\/body&gt;<br \/>\n&lt;\/html&gt;<\/p>\n<h5>31.2.2 \u6e10\u8fdb\u5f0f\u9519\u8bef\u5904\u7406<\/h5>\n<p>javascript<\/p>\n<p>\/\/ \u6e10\u8fdb\u5f0f\u589e\u5f3a\u7684\u9519\u8bef\u5904\u7406<br \/>\nclass ProgressiveErrorHandler {<br \/>\n  constructor() {<br \/>\n    this.errorQueue &#061; [];<br \/>\n    this.maxRetries &#061; 3;<br \/>\n    this.retryDelays &#061; [1000, 3000, 10000]; \/\/ \u6307\u6570\u9000\u907f<br \/>\n  }<\/p>\n<p>  async handleRequest(requestFunc, options &#061; {}) {<br \/>\n    const {<br \/>\n      retryCount &#061; 0,<br \/>\n      userMessage &#061; &#039;\u64cd\u4f5c\u5931\u8d25&#xff0c;\u8bf7\u91cd\u8bd5&#039;,<br \/>\n      fallbackAction &#061; null<br \/>\n    } &#061; options;<\/p>\n<p>    try {<br \/>\n      return await requestFunc();<br \/>\n    } catch (error) {<br \/>\n      \/\/ \u8bb0\u5f55\u9519\u8bef<br \/>\n      this.logError(error);<\/p>\n<p>      \/\/ \u6839\u636e\u9519\u8bef\u7c7b\u578b\u5904\u7406<br \/>\n      switch (error.code) {<br \/>\n        case &#039;NETWORK_ERROR&#039;:<br \/>\n          return this.handleNetworkError(error, {<br \/>\n            requestFunc,<br \/>\n            retryCount,<br \/>\n            userMessage,<br \/>\n            fallbackAction<br \/>\n          });<\/p>\n<p>        case &#039;SERVER_ERROR&#039;:<br \/>\n          return this.handleServerError(error, {<br \/>\n            requestFunc,<br \/>\n            retryCount,<br \/>\n            userMessage,<br \/>\n            fallbackAction<br \/>\n          });<\/p>\n<p>        case &#039;RATE_LIMITED&#039;:<br \/>\n          return this.handleRateLimit(error, {<br \/>\n            requestFunc,<br \/>\n            retryCount,<br \/>\n            userMessage<br \/>\n          });<\/p>\n<p>        default:<br \/>\n          return this.showUserError(error, userMessage, fallbackAction);<br \/>\n      }<br \/>\n    }<br \/>\n  }<\/p>\n<p>  async handleNetworkError(error, options) {<br \/>\n    const { requestFunc, retryCount, userMessage, fallbackAction } &#061; options;<\/p>\n<p>    if (retryCount &lt; this.maxRetries) {<br \/>\n      \/\/ \u663e\u793a\u91cd\u8bd5\u63d0\u793a<br \/>\n      this.showToast({<br \/>\n        type: &#039;warning&#039;,<br \/>\n        message: &#096;\u7f51\u7edc\u8fde\u63a5\u4e0d\u7a33\u5b9a&#xff0c;${retryCount &#043; 1}\u79d2\u540e\u91cd\u8bd5&#8230;&#096;,<br \/>\n        duration: retryCount &#043; 1 * 1000<br \/>\n      });<\/p>\n<p>      \/\/ \u5ef6\u8fdf\u91cd\u8bd5<br \/>\n      await this.delay(this.retryDelays[retryCount]);<\/p>\n<p>      return this.handleRequest(requestFunc, {<br \/>\n        &#8230;options,<br \/>\n        retryCount: retryCount &#043; 1<br \/>\n      });<br \/>\n    }<\/p>\n<p>    \/\/ \u8fbe\u5230\u6700\u5927\u91cd\u8bd5\u6b21\u6570<br \/>\n    return this.showOfflineMode(error, userMessage, fallbackAction);<br \/>\n  }<\/p>\n<p>  async handleServerError(error, options) {<br \/>\n    const { status, originalError } &#061; error;<\/p>\n<p>    if ([502, 503, 504].includes(status)) {<br \/>\n      \/\/ \u4e34\u65f6\u6027\u670d\u52a1\u5668\u9519\u8bef&#xff0c;\u53ef\u4ee5\u91cd\u8bd5<br \/>\n      if (options.retryCount &lt; 2) { \/\/ \u670d\u52a1\u5668\u9519\u8bef\u53ea\u91cd\u8bd52\u6b21<br \/>\n        await this.delay(this.retryDelays[options.retryCount]);<\/p>\n<p>        return this.handleRequest(options.requestFunc, {<br \/>\n          &#8230;options,<br \/>\n          retryCount: options.retryCount &#043; 1<br \/>\n        });<br \/>\n      }<br \/>\n    }<\/p>\n<p>    \/\/ \u6c38\u4e45\u6027\u670d\u52a1\u5668\u9519\u8bef\u6216\u8fbe\u5230\u91cd\u8bd5\u6b21\u6570<br \/>\n    return this.showUserError(error, options.userMessage, options.fallbackAction);<br \/>\n  }<\/p>\n<p>  async handleRateLimit(error, options) {<br \/>\n    const { retryAfter } &#061; error;<br \/>\n    const waitTime &#061; retryAfter ? parseInt(retryAfter) * 1000 : 60000;<\/p>\n<p>    this.showToast({<br \/>\n      type: &#039;info&#039;,<br \/>\n      message: &#096;\u64cd\u4f5c\u8fc7\u4e8e\u9891\u7e41&#xff0c;\u8bf7${Math.ceil(waitTime \/ 1000)}\u79d2\u540e\u91cd\u8bd5&#096;,<br \/>\n      duration: 5000<br \/>\n    });<\/p>\n<p>    await this.delay(waitTime);<\/p>\n<p>    return this.handleRequest(options.requestFunc, {<br \/>\n      &#8230;options,<br \/>\n      retryCount: options.retryCount &#043; 1<br \/>\n    });<br \/>\n  }<\/p>\n<p>  showOfflineMode(error, userMessage, fallbackAction) {<br \/>\n    \/\/ \u663e\u793a\u79bb\u7ebf\u6a21\u5f0fUI<br \/>\n    this.showOfflineUI();<\/p>\n<p>    \/\/ \u5982\u679c\u6709\u5907\u7528\u64cd\u4f5c&#xff0c;\u6267\u884c\u5b83<br \/>\n    if (fallbackAction) {<br \/>\n      return fallbackAction();<br \/>\n    }<\/p>\n<p>    \/\/ \u5426\u5219\u8fd4\u56de\u4e00\u4e2a\u7279\u6b8a\u7684\u79bb\u7ebf\u7ed3\u679c<br \/>\n    return {<br \/>\n      success: false,<br \/>\n      offline: true,<br \/>\n      message: &#039;\u5f53\u524d\u5904\u4e8e\u79bb\u7ebf\u6a21\u5f0f&#xff0c;\u6570\u636e\u5c06\u5728\u7f51\u7edc\u6062\u590d\u540e\u540c\u6b65&#039;<br \/>\n    };<br \/>\n  }<\/p>\n<p>  showUserError(error, userMessage, fallbackAction) {<br \/>\n    \/\/ \u663e\u793a\u7528\u6237\u53cb\u597d\u7684\u9519\u8bef\u63d0\u793a<br \/>\n    this.showDialog({<br \/>\n      title: &#039;\u64cd\u4f5c\u5931\u8d25&#039;,<br \/>\n      message: userMessage,<br \/>\n      type: &#039;error&#039;,<br \/>\n      buttons: [<br \/>\n        {<br \/>\n          text: &#039;\u91cd\u8bd5&#039;,<br \/>\n          action: () &#061;&gt; {<br \/>\n            if (fallbackAction) {<br \/>\n              fallbackAction();<br \/>\n            } else {<br \/>\n              window.location.reload();<br \/>\n            }<br \/>\n          }<br \/>\n        },<br \/>\n        {<br \/>\n          text: &#039;\u8054\u7cfb\u652f\u6301&#039;,<br \/>\n          action: () &#061;&gt; {<br \/>\n            window.open(&#039;\/contact&#039;, &#039;_blank&#039;);<br \/>\n          }<br \/>\n        }<br \/>\n      ]<br \/>\n    });<\/p>\n<p>    return {<br \/>\n      success: false,<br \/>\n      error: error.code || &#039;UNKNOWN_ERROR&#039;,<br \/>\n      message: userMessage<br \/>\n    };<br \/>\n  }<\/p>\n<p>  logError(error) {<br \/>\n    \/\/ \u53d1\u9001\u9519\u8bef\u5230\u76d1\u63a7\u7cfb\u7edf<br \/>\n    if (window.analytics) {<br \/>\n      window.analytics.track(&#039;error_occurred&#039;, {<br \/>\n        error_code: error.code,<br \/>\n        error_message: error.message,<br \/>\n        url: window.location.href,<br \/>\n        timestamp: new Date().toISOString()<br \/>\n      });<br \/>\n    }<\/p>\n<p>    \/\/ \u672c\u5730\u5b58\u50a8\u9519\u8bef\u961f\u5217&#xff08;\u7528\u4e8e\u79bb\u7ebf\u65f6\u91cd\u8bd5&#xff09;<br \/>\n    this.errorQueue.push({<br \/>\n      error,<br \/>\n      timestamp: Date.now(),<br \/>\n      context: {<br \/>\n        url: window.location.href,<br \/>\n        userAgent: navigator.userAgent<br \/>\n      }<br \/>\n    });<\/p>\n<p>    \/\/ \u4fdd\u6301\u961f\u5217\u5927\u5c0f<br \/>\n    if (this.errorQueue.length &gt; 100) {<br \/>\n      this.errorQueue.shift();<br \/>\n    }<\/p>\n<p>    \/\/ \u5b58\u50a8\u5230localStorage<br \/>\n    localStorage.setItem(&#039;error_queue&#039;, JSON.stringify(this.errorQueue));<br \/>\n  }<\/p>\n<p>  showToast(options) {<br \/>\n    \/\/ \u5b9e\u73b0Toast\u63d0\u793a<br \/>\n    const toast &#061; document.createElement(&#039;div&#039;);<br \/>\n    toast.className &#061; &#096;toast toast-${options.type}&#096;;<br \/>\n    toast.textContent &#061; options.message;<br \/>\n    toast.style.cssText &#061; &#096;<br \/>\n      position: fixed;<br \/>\n      top: 20px;<br \/>\n      right: 20px;<br \/>\n      padding: 12px 24px;<br \/>\n      border-radius: 6px;<br \/>\n      color: white;<br \/>\n      background: ${options.type &#061;&#061;&#061; &#039;warning&#039; ? &#039;#f0ad4e&#039; : &#039;#5bc0de&#039;};<br \/>\n      z-index: 10000;<br \/>\n      box-shadow: 0 4px 12px rgba(0,0,0,0.15);<br \/>\n      animation: slideIn 0.3s ease;<br \/>\n    &#096;;<\/p>\n<p>    document.body.appendChild(toast);<\/p>\n<p>    setTimeout(() &#061;&gt; {<br \/>\n      toast.style.animation &#061; &#039;slideOut 0.3s ease&#039;;<br \/>\n      setTimeout(() &#061;&gt; toast.remove(), 300);<br \/>\n    }, options.duration || 3000);<br \/>\n  }<\/p>\n<p>  delay(ms) {<br \/>\n    return new Promise(resolve &#061;&gt; setTimeout(resolve, ms));<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ \u4f7f\u7528\u793a\u4f8b<br \/>\nconst errorHandler &#061; new ProgressiveErrorHandler();<\/p>\n<p>async function loadUserProfile() {<br \/>\n  return errorHandler.handleRequest(<br \/>\n    async () &#061;&gt; {<br \/>\n      const response &#061; await fetch(&#039;\/api\/user\/profile&#039;);<br \/>\n      if (!response.ok) {<br \/>\n        throw {<br \/>\n          code: response.status &gt;&#061; 500 ? &#039;SERVER_ERROR&#039; : &#039;CLIENT_ERROR&#039;,<br \/>\n          status: response.status,<br \/>\n          message: &#039;Failed to load profile&#039;<br \/>\n        };<br \/>\n      }<br \/>\n      return response.json();<br \/>\n    },<br \/>\n    {<br \/>\n      userMessage: &#039;\u52a0\u8f7d\u4e2a\u4eba\u8d44\u6599\u5931\u8d25&#039;,<br \/>\n      fallbackAction: () &#061;&gt; {<br \/>\n        \/\/ \u663e\u793a\u7f13\u5b58\u7684\u7528\u6237\u6570\u636e<br \/>\n        const cached &#061; localStorage.getItem(&#039;cached_profile&#039;);<br \/>\n        if (cached) {<br \/>\n          return JSON.parse(cached);<br \/>\n        }<br \/>\n      }<br \/>\n    }<br \/>\n  );<br \/>\n}<\/p>\n<h4>31.3 \u6027\u80fd\u4f18\u5316\u4e0e\u72b6\u6001\u7801<\/h4>\n<h5>31.3.1 \u7f13\u5b58\u7b56\u7565\u4e0e\u72b6\u6001\u7801<\/h5>\n<p>http<\/p>\n<p># \u7f13\u5b58\u63a7\u5236\u5934\u793a\u4f8b<br \/>\nGET \/api\/products\/123 HTTP\/1.1<\/p>\n<p>HTTP\/1.1 200 OK<br \/>\nContent-Type: application\/json<br \/>\nCache-Control: public, max-age&#061;3600, stale-while-revalidate&#061;7200<br \/>\nETag: &#034;abc123def456&#034;<br \/>\nLast-Modified: Mon, 15 Jan 2024 10:00:00 GMT<\/p>\n<p>{<br \/>\n  &#034;id&#034;: &#034;123&#034;,<br \/>\n  &#034;name&#034;: &#034;Product Name&#034;,<br \/>\n  &#034;price&#034;: 99.99<br \/>\n}<\/p>\n<p>Service Worker\u4e2d\u7684\u7f13\u5b58\u7b56\u7565<\/p>\n<p>javascript<\/p>\n<p>\/\/ service-worker.js<br \/>\nconst CACHE_NAME &#061; &#039;app-v1&#039;;<br \/>\nconst STATIC_CACHE &#061; &#039;static-v1&#039;;<\/p>\n<p>\/\/ \u5b89\u88c5\u65f6\u7f13\u5b58\u9759\u6001\u8d44\u6e90<br \/>\nself.addEventListener(&#039;install&#039;, event &#061;&gt; {<br \/>\n  event.waitUntil(<br \/>\n    caches.open(STATIC_CACHE).then(cache &#061;&gt; {<br \/>\n      return cache.addAll([<br \/>\n        &#039;\/&#039;,<br \/>\n        &#039;\/index.html&#039;,<br \/>\n        &#039;\/styles\/main.css&#039;,<br \/>\n        &#039;\/scripts\/app.js&#039;,<br \/>\n        &#039;\/offline.html&#039;<br \/>\n      ]);<br \/>\n    })<br \/>\n  );<br \/>\n});<\/p>\n<p>\/\/ \u62e6\u622a\u8bf7\u6c42<br \/>\nself.addEventListener(&#039;fetch&#039;, event &#061;&gt; {<br \/>\n  \/\/ API\u8bf7\u6c42 &#8211; \u7f51\u7edc\u4f18\u5148&#xff0c;\u5931\u8d25\u65f6\u4f7f\u7528\u7f13\u5b58<br \/>\n  if (event.request.url.includes(&#039;\/api\/&#039;)) {<br \/>\n    event.respondWith(<br \/>\n      fetch(event.request)<br \/>\n        .then(response &#061;&gt; {<br \/>\n          \/\/ \u7f13\u5b58\u6210\u529f\u7684\u54cd\u5e94<br \/>\n          if (response.status &#061;&#061;&#061; 200) {<br \/>\n            const clonedResponse &#061; response.clone();<br \/>\n            caches.open(CACHE_NAME).then(cache &#061;&gt; {<br \/>\n              cache.put(event.request, clonedResponse);<br \/>\n            });<br \/>\n          }<br \/>\n          return response;<br \/>\n        })<br \/>\n        .catch(() &#061;&gt; {<br \/>\n          \/\/ \u7f51\u7edc\u5931\u8d25&#xff0c;\u5c1d\u8bd5\u4ece\u7f13\u5b58\u83b7\u53d6<br \/>\n          return caches.match(event.request).then(cachedResponse &#061;&gt; {<br \/>\n            if (cachedResponse) {<br \/>\n              \/\/ \u6dfb\u52a0\u7f13\u5b58\u6807\u8bc6\u5934<br \/>\n              const headers &#061; new Headers(cachedResponse.headers);<br \/>\n              headers.set(&#039;X-Cache&#039;, &#039;HIT&#039;);<\/p>\n<p>              return new Response(cachedResponse.body, {<br \/>\n                status: cachedResponse.status,<br \/>\n                statusText: cachedResponse.statusText,<br \/>\n                headers<br \/>\n              });<br \/>\n            }<\/p>\n<p>            \/\/ \u8fde\u7f13\u5b58\u4e5f\u6ca1\u6709&#xff0c;\u8fd4\u56de\u79bb\u7ebf\u9875\u9762<br \/>\n            return caches.match(&#039;\/offline.html&#039;);<br \/>\n          });<br \/>\n        })<br \/>\n    );<br \/>\n  } else {<br \/>\n    \/\/ \u9759\u6001\u8d44\u6e90 &#8211; \u7f13\u5b58\u4f18\u5148<br \/>\n    event.respondWith(<br \/>\n      caches.match(event.request).then(cachedResponse &#061;&gt; {<br \/>\n        if (cachedResponse) {<br \/>\n          \/\/ \u6dfb\u52a0\u7f13\u5b58\u6807\u8bc6<br \/>\n          const headers &#061; new Headers(cachedResponse.headers);<br \/>\n          headers.set(&#039;X-Cache&#039;, &#039;HIT&#039;);<\/p>\n<p>          return new Response(cachedResponse.body, {<br \/>\n            status: cachedResponse.status,<br \/>\n            statusText: cachedResponse.statusText,<br \/>\n            headers<br \/>\n          });<br \/>\n        }<\/p>\n<p>        return fetch(event.request).then(response &#061;&gt; {<br \/>\n          \/\/ \u53ea\u7f13\u5b58\u6210\u529f\u7684\u54cd\u5e94<br \/>\n          if (response.status &#061;&#061;&#061; 200) {<br \/>\n            const clonedResponse &#061; response.clone();<br \/>\n            caches.open(STATIC_CACHE).then(cache &#061;&gt; {<br \/>\n              cache.put(event.request, clonedResponse);<br \/>\n            });<br \/>\n          }<br \/>\n          return response;<br \/>\n        });<br \/>\n      })<br \/>\n    );<br \/>\n  }<br \/>\n});<\/p>\n<p>\/\/ \u6fc0\u6d3b\u65f6\u6e05\u7406\u65e7\u7f13\u5b58<br \/>\nself.addEventListener(&#039;activate&#039;, event &#061;&gt; {<br \/>\n  event.waitUntil(<br \/>\n    caches.keys().then(cacheNames &#061;&gt; {<br \/>\n      return Promise.all(<br \/>\n        cacheNames.map(cacheName &#061;&gt; {<br \/>\n          if (cacheName !&#061;&#061; CACHE_NAME &amp;&amp; cacheName !&#061;&#061; STATIC_CACHE) {<br \/>\n            return caches.delete(cacheName);<br \/>\n          }<br \/>\n        })<br \/>\n      );<br \/>\n    })<br \/>\n  );<br \/>\n});<\/p>\n<h5>31.3.2 \u9884\u52a0\u8f7d\u4e0e\u9884\u8fde\u63a5<\/h5>\n<p>html<\/p>\n<p>&lt;!&#8211; \u6587\u6863\u5934\u90e8\u6dfb\u52a0\u9884\u52a0\u8f7d\u63d0\u793a &#8211;&gt;<br \/>\n&lt;head&gt;<br \/>\n    &lt;!&#8211; \u9884\u52a0\u8f7d\u5173\u952eCSS &#8211;&gt;<br \/>\n    &lt;link rel&#061;&#034;preload&#034; href&#061;&#034;\/styles\/critical.css&#034; as&#061;&#034;style&#034;&gt;<\/p>\n<p>    &lt;!&#8211; \u9884\u52a0\u8f7d\u5173\u952e\u5b57\u4f53 &#8211;&gt;<br \/>\n    &lt;link rel&#061;&#034;preload&#034; href&#061;&#034;\/fonts\/iconfont.woff2&#034; as&#061;&#034;font&#034; type&#061;&#034;font\/woff2&#034; crossorigin&gt;<\/p>\n<p>    &lt;!&#8211; \u9884\u8fde\u63a5API\u57df\u540d &#8211;&gt;<br \/>\n    &lt;link rel&#061;&#034;preconnect&#034; href&#061;&#034;https:\/\/api.example.com&#034;&gt;<br \/>\n    &lt;link rel&#061;&#034;dns-prefetch&#034; href&#061;&#034;https:\/\/api.example.com&#034;&gt;<\/p>\n<p>    &lt;!&#8211; \u9884\u52a0\u8f7d\u91cd\u8981API\u7aef\u70b9 &#8211;&gt;<br \/>\n    &lt;link rel&#061;&#034;prefetch&#034; href&#061;&#034;\/api\/user\/profile&#034; as&#061;&#034;fetch&#034; crossorigin&gt;<\/p>\n<p>    &lt;!&#8211; \u72b6\u6001\u7801\u76f8\u5173\u7684\u5907\u7528\u8d44\u6e90 &#8211;&gt;<br \/>\n    &lt;noscript&gt;<br \/>\n        &lt;!&#8211; \u65e0JavaScript\u65f6\u7684\u964d\u7ea7\u5904\u7406 &#8211;&gt;<br \/>\n        &lt;meta http-equiv&#061;&#034;refresh&#034; content&#061;&#034;0;url&#061;\/noscript.html&#034;&gt;<br \/>\n    &lt;\/noscript&gt;<br \/>\n&lt;\/head&gt;<\/p>\n<h4>31.4 \u5b89\u5168\u6700\u4f73\u5b9e\u8df5<\/h4>\n<h5>31.4.1 \u8de8\u7ad9\u8bf7\u6c42\u4f2a\u9020&#xff08;CSRF&#xff09;\u9632\u62a4<\/h5>\n<p>javascript<\/p>\n<p>\/\/ \u670d\u52a1\u7aefCSRF\u9632\u62a4\u4e2d\u95f4\u4ef6<br \/>\nconst csrfProtection &#061; (req, res, next) &#061;&gt; {<br \/>\n  \/\/ \u6392\u9664\u67d0\u4e9b\u8def\u7531&#xff08;\u5982API\u6587\u6863&#xff09;<br \/>\n  if (req.path.startsWith(&#039;\/api-docs&#039;)) {<br \/>\n    return next();<br \/>\n  }<\/p>\n<p>  \/\/ \u5b89\u5168\u65b9\u6cd5\u4e0d\u9700\u8981CSRF\u4fdd\u62a4<br \/>\n  const safeMethods &#061; [&#039;GET&#039;, &#039;HEAD&#039;, &#039;OPTIONS&#039;];<br \/>\n  if (safeMethods.includes(req.method)) {<br \/>\n    return next();<br \/>\n  }<\/p>\n<p>  \/\/ \u9a8c\u8bc1CSRF\u4ee4\u724c<br \/>\n  const csrfToken &#061; req.headers[&#039;x-csrf-token&#039;] || req.body._csrf;<br \/>\n  const sessionToken &#061; req.session.csrfToken;<\/p>\n<p>  if (!csrfToken || csrfToken !&#061;&#061; sessionToken) {<br \/>\n    \/\/ \u8bb0\u5f55\u5b89\u5168\u4e8b\u4ef6<br \/>\n    logSecurityEvent({<br \/>\n      type: &#039;CSRF_ATTEMPT&#039;,<br \/>\n      ip: req.ip,<br \/>\n      userAgent: req.get(&#039;User-Agent&#039;),<br \/>\n      path: req.path,<br \/>\n      timestamp: new Date()<br \/>\n    });<\/p>\n<p>    \/\/ \u8fd4\u56de403\u72b6\u6001\u7801<br \/>\n    return res.status(403).json({<br \/>\n      error: {<br \/>\n        code: &#039;CSRF_TOKEN_INVALID&#039;,<br \/>\n        message: &#039;Invalid or missing CSRF token&#039;,<br \/>\n        documentation: &#039;https:\/\/api.example.com\/docs\/security#csrf-protection&#039;<br \/>\n      }<br \/>\n    });<br \/>\n  }<\/p>\n<p>  \/\/ \u4ee4\u724c\u9a8c\u8bc1\u6210\u529f&#xff0c;\u751f\u6210\u65b0\u4ee4\u724c&#xff08;\u53cc\u63d0\u4ea4cookie\u6a21\u5f0f&#xff09;<br \/>\n  const newToken &#061; generateCsrfToken();<br \/>\n  req.session.csrfToken &#061; newToken;<br \/>\n  res.cookie(&#039;XSRF-TOKEN&#039;, newToken, {<br \/>\n    httpOnly: false, \/\/ \u9700\u8981\u5ba2\u6237\u7aefJavaScript\u8bbf\u95ee<br \/>\n    secure: process.env.NODE_ENV &#061;&#061;&#061; &#039;production&#039;,<br \/>\n    sameSite: &#039;strict&#039;<br \/>\n  });<\/p>\n<p>  next();<br \/>\n};<\/p>\n<p>\/\/ \u5ba2\u6237\u7aef\u81ea\u52a8\u53d1\u9001CSRF\u4ee4\u724c<br \/>\naxios.interceptors.request.use(config &#061;&gt; {<br \/>\n  const token &#061; getCsrfToken(); \/\/ \u4ececookie\u6216meta\u6807\u7b7e\u83b7\u53d6<\/p>\n<p>  if (token &amp;&amp; !config.headers[&#039;X-CSRF-Token&#039;]) {<br \/>\n    config.headers[&#039;X-CSRF-Token&#039;] &#061; token;<br \/>\n  }<\/p>\n<p>  return config;<br \/>\n});<\/p>\n<h5>31.4.2 \u5185\u5bb9\u5b89\u5168\u7b56\u7565&#xff08;CSP&#xff09;\u4e0e\u72b6\u6001\u7801<\/h5>\n<p>javascript<\/p>\n<p>\/\/ Express\u4e2d\u95f4\u4ef6\u8bbe\u7f6eCSP<br \/>\napp.use((req, res, next) &#061;&gt; {<br \/>\n  const nonce &#061; crypto.randomBytes(16).toString(&#039;base64&#039;);<br \/>\n  res.locals.cspNonce &#061; nonce;<\/p>\n<p>  res.setHeader(<br \/>\n    &#039;Content-Security-Policy&#039;,<br \/>\n    &#096;default-src &#039;self&#039;;<br \/>\n     script-src &#039;self&#039; &#039;nonce-${nonce}&#039; https:\/\/analytics.example.com;<br \/>\n     style-src &#039;self&#039; &#039;unsafe-inline&#039;;<br \/>\n     img-src &#039;self&#039; data: https:\/\/cdn.example.com;<br \/>\n     font-src &#039;self&#039; https:\/\/fonts.googleapis.com;<br \/>\n     connect-src &#039;self&#039; https:\/\/api.example.com;<br \/>\n     frame-ancestors &#039;none&#039;;<br \/>\n     form-action &#039;self&#039;;<br \/>\n     base-uri &#039;self&#039;;&#096;<br \/>\n  );<\/p>\n<p>  next();<br \/>\n});<\/p>\n<p>\/\/ \u5728HTML\u6a21\u677f\u4e2d\u4f7f\u7528nonce<br \/>\napp.get(&#039;\/&#039;, (req, res) &#061;&gt; {<br \/>\n  res.render(&#039;index&#039;, {<br \/>\n    nonce: res.locals.cspNonce<br \/>\n  });<br \/>\n});<\/p>\n<p>html<\/p>\n<p>&lt;!&#8211; \u6a21\u677f\u4e2d\u4f7f\u7528nonce &#8211;&gt;<br \/>\n&lt;!DOCTYPE html&gt;<br \/>\n&lt;html&gt;<br \/>\n&lt;head&gt;<br \/>\n    &lt;!&#8211; \u5185\u8054\u6837\u5f0f\u4ecd\u7136\u5141\u8bb8 &#8211;&gt;<br \/>\n    &lt;style&gt;<br \/>\n        .error { color: red; }<br \/>\n    &lt;\/style&gt;<br \/>\n&lt;\/head&gt;<br \/>\n&lt;body&gt;<br \/>\n    &lt;!&#8211; \u4f7f\u7528nonce\u7684\u5185\u8054\u811a\u672c &#8211;&gt;<br \/>\n    &lt;script nonce&#061;&#034;{{nonce}}&#034;&gt;<br \/>\n        window.config &#061; {<br \/>\n            apiUrl: &#039;{{apiUrl}}&#039;,<br \/>\n            userId: &#039;{{userId}}&#039;<br \/>\n        };<br \/>\n    &lt;\/script&gt;<\/p>\n<p>    &lt;!&#8211; \u5916\u90e8\u811a\u672c &#8211;&gt;<br \/>\n    &lt;script src&#061;&#034;\/app.js&#034; nonce&#061;&#034;{{nonce}}&#034;&gt;&lt;\/script&gt;<\/p>\n<p>    &lt;!&#8211; \u88abCSP\u963b\u6b62\u7684\u811a\u672c &#8211;&gt;<br \/>\n    &lt;script&gt;<br \/>\n        \/\/ \u8fd9\u4e2a\u811a\u672c\u4f1a\u88abCSP\u963b\u6b62&#xff0c;\u56e0\u4e3a\u6ca1\u6709nonce<br \/>\n        console.log(&#039;This will be blocked&#039;);<br \/>\n    &lt;\/script&gt;<br \/>\n&lt;\/body&gt;<br \/>\n&lt;\/html&gt;<\/p>\n<h4>31.5 \u76d1\u63a7\u4e0e\u65e5\u5fd7<\/h4>\n<h5>31.5.1 \u7ed3\u6784\u5316\u9519\u8bef\u65e5\u5fd7<\/h5>\n<p>javascript<\/p>\n<p>\/\/ Winston\u65e5\u5fd7\u914d\u7f6e<br \/>\nconst winston &#061; require(&#039;winston&#039;);<br \/>\nconst { combine, timestamp, json, errors } &#061; winston.format;<\/p>\n<p>const logger &#061; winston.createLogger({<br \/>\n  level: process.env.LOG_LEVEL || &#039;info&#039;,<br \/>\n  format: combine(<br \/>\n    timestamp(),<br \/>\n    errors({ stack: true }),<br \/>\n    json()<br \/>\n  ),<br \/>\n  transports: [<br \/>\n    \/\/ \u63a7\u5236\u53f0\u8f93\u51fa&#xff08;\u5f00\u53d1\u73af\u5883&#xff09;<br \/>\n    new winston.transports.Console({<br \/>\n      format: winston.format.simple()<br \/>\n    }),<\/p>\n<p>    \/\/ \u9519\u8bef\u65e5\u5fd7\u6587\u4ef6<br \/>\n    new winston.transports.File({<br \/>\n      filename: &#039;logs\/error.log&#039;,<br \/>\n      level: &#039;error&#039;,<br \/>\n      maxsize: 5242880, \/\/ 5MB<br \/>\n      maxFiles: 5<br \/>\n    }),<\/p>\n<p>    \/\/ \u8bbf\u95ee\u65e5\u5fd7\u6587\u4ef6<br \/>\n    new winston.transports.File({<br \/>\n      filename: &#039;logs\/access.log&#039;,<br \/>\n      level: &#039;info&#039;,<br \/>\n      maxsize: 5242880,<br \/>\n      maxFiles: 10<br \/>\n    })<br \/>\n  ]<br \/>\n});<\/p>\n<p>\/\/ Express\u8bbf\u95ee\u65e5\u5fd7\u4e2d\u95f4\u4ef6<br \/>\napp.use((req, res, next) &#061;&gt; {<br \/>\n  const startTime &#061; Date.now();<\/p>\n<p>  \/\/ \u6355\u83b7\u54cd\u5e94\u5b8c\u6210\u4e8b\u4ef6<br \/>\n  res.on(&#039;finish&#039;, () &#061;&gt; {<br \/>\n    const duration &#061; Date.now() &#8211; startTime;<\/p>\n<p>    logger.info(&#039;HTTP Request&#039;, {<br \/>\n      timestamp: new Date().toISOString(),<br \/>\n      method: req.method,<br \/>\n      url: req.originalUrl,<br \/>\n      statusCode: res.statusCode,<br \/>\n      statusMessage: res.statusMessage,<br \/>\n      duration,<br \/>\n      userAgent: req.get(&#039;User-Agent&#039;),<br \/>\n      ip: req.ip,<br \/>\n      userId: req.user?.id || &#039;anonymous&#039;,<br \/>\n      requestId: req.id,<br \/>\n      \/\/ \u7279\u5b9a\u72b6\u6001\u7801\u7684\u989d\u5916\u4fe1\u606f<br \/>\n      &#8230;(res.statusCode &gt;&#061; 400 &amp;&amp; {<br \/>\n        errorDetails: {<br \/>\n          message: res.locals.error?.message,<br \/>\n          stack: process.env.NODE_ENV &#061;&#061;&#061; &#039;development&#039;<br \/>\n            ? res.locals.error?.stack<br \/>\n            : undefined<br \/>\n        }<br \/>\n      }),<br \/>\n      &#8230;(res.statusCode &#061;&#061;&#061; 429 &amp;&amp; {<br \/>\n        rateLimit: {<br \/>\n          limit: req.rateLimit?.limit,<br \/>\n          remaining: req.rateLimit?.remaining,<br \/>\n          resetTime: req.rateLimit?.resetTime<br \/>\n        }<br \/>\n      })<br \/>\n    });<br \/>\n  });<\/p>\n<p>  next();<br \/>\n});<\/p>\n<p>\/\/ \u9519\u8bef\u5904\u7406\u4e2d\u95f4\u4ef6<br \/>\napp.use((error, req, res, next) &#061;&gt; {<br \/>\n  \/\/ \u8bb0\u5f55\u9519\u8bef<br \/>\n  logger.error(&#039;Application Error&#039;, {<br \/>\n    timestamp: new Date().toISOString(),<br \/>\n    requestId: req.id,<br \/>\n    url: req.originalUrl,<br \/>\n    method: req.method,<br \/>\n    userId: req.user?.id,<br \/>\n    error: {<br \/>\n      name: error.name,<br \/>\n      message: error.message,<br \/>\n      stack: error.stack,<br \/>\n      code: error.code,<br \/>\n      statusCode: error.statusCode<br \/>\n    }<br \/>\n  });<\/p>\n<p>  \/\/ \u6839\u636e\u73af\u5883\u8fd4\u56de\u9519\u8bef<br \/>\n  const isProduction &#061; process.env.NODE_ENV &#061;&#061;&#061; &#039;production&#039;;<\/p>\n<p>  res.status(error.statusCode || 500).json({<br \/>\n    error: {<br \/>\n      code: error.code || &#039;INTERNAL_ERROR&#039;,<br \/>\n      message: isProduction ? &#039;Internal server error&#039; : error.message,<br \/>\n      &#8230;(!isProduction &amp;&amp; { stack: error.stack }),<br \/>\n      requestId: req.id,<br \/>\n      timestamp: new Date().toISOString()<br \/>\n    }<br \/>\n  });<br \/>\n});<\/p>\n<h5>31.5.2 \u5b9e\u65f6\u76d1\u63a7\u4eea\u8868\u677f<\/h5>\n<p>javascript<\/p>\n<p>\/\/ WebSocket\u5b9e\u65f6\u76d1\u63a7<br \/>\nconst WebSocket &#061; require(&#039;ws&#039;);<br \/>\nconst wss &#061; new WebSocket.Server({ port: 8080 });<\/p>\n<p>\/\/ \u72b6\u6001\u7801\u7edf\u8ba1<br \/>\nconst statusStats &#061; {<br \/>\n  &#039;1xx&#039;: 0, &#039;2xx&#039;: 0, &#039;3xx&#039;: 0, &#039;4xx&#039;: 0, &#039;5xx&#039;: 0,<br \/>\n  details: {}<br \/>\n};<\/p>\n<p>\/\/ \u5b9e\u65f6\u9519\u8bef\u961f\u5217<br \/>\nconst recentErrors &#061; [];<\/p>\n<p>wss.on(&#039;connection&#039;, (ws) &#061;&gt; {<br \/>\n  \/\/ \u53d1\u9001\u5f53\u524d\u72b6\u6001<br \/>\n  ws.send(JSON.stringify({<br \/>\n    type: &#039;INITIAL_STATS&#039;,<br \/>\n    data: statusStats<br \/>\n  }));<\/p>\n<p>  \/\/ \u53d1\u9001\u6700\u8fd1\u9519\u8bef<br \/>\n  ws.send(JSON.stringify({<br \/>\n    type: &#039;RECENT_ERRORS&#039;,<br \/>\n    data: recentErrors.slice(-50) \/\/ \u6700\u8fd150\u4e2a\u9519\u8bef<br \/>\n  }));<br \/>\n});<\/p>\n<p>\/\/ \u66f4\u65b0\u7edf\u8ba1\u4fe1\u606f<br \/>\nfunction updateStats(statusCode) {<br \/>\n  const statusClass &#061; &#096;${Math.floor(statusCode \/ 100)}xx&#096;;<br \/>\n  statusStats[statusClass]&#043;&#043;;<\/p>\n<p>  if (!statusStats.details[statusCode]) {<br \/>\n    statusStats.details[statusCode] &#061; 0;<br \/>\n  }<br \/>\n  statusStats.details[statusCode]&#043;&#043;;<\/p>\n<p>  \/\/ \u5e7f\u64ad\u66f4\u65b0<br \/>\n  broadcast({<br \/>\n    type: &#039;STATS_UPDATE&#039;,<br \/>\n    data: {<br \/>\n      statusClass,<br \/>\n      statusCode,<br \/>\n      counts: statusStats<br \/>\n    }<br \/>\n  });<br \/>\n}<\/p>\n<p>\/\/ \u6dfb\u52a0\u9519\u8bef\u5230\u961f\u5217<br \/>\nfunction addError(error) {<br \/>\n  recentErrors.push({<br \/>\n    &#8230;error,<br \/>\n    timestamp: new Date().toISOString()<br \/>\n  });<\/p>\n<p>  \/\/ \u4fdd\u6301\u961f\u5217\u5927\u5c0f<br \/>\n  if (recentErrors.length &gt; 1000) {<br \/>\n    recentErrors.shift();<br \/>\n  }<\/p>\n<p>  \/\/ \u5e7f\u64ad\u65b0\u9519\u8bef<br \/>\n  broadcast({<br \/>\n    type: &#039;NEW_ERROR&#039;,<br \/>\n    data: error<br \/>\n  });<br \/>\n}<\/p>\n<p>\/\/ \u5e7f\u64ad\u6d88\u606f\u7ed9\u6240\u6709\u5ba2\u6237\u7aef<br \/>\nfunction broadcast(message) {<br \/>\n  wss.clients.forEach(client &#061;&gt; {<br \/>\n    if (client.readyState &#061;&#061;&#061; WebSocket.OPEN) {<br \/>\n      client.send(JSON.stringify(message));<br \/>\n    }<br \/>\n  });<br \/>\n}<\/p>\n<p>\/\/ \u6bcf\u5c0f\u65f6\u91cd\u7f6e\u8ba1\u6570\u5668<br \/>\nsetInterval(() &#061;&gt; {<br \/>\n  statusStats[&#039;1xx&#039;] &#061; 0;<br \/>\n  statusStats[&#039;2xx&#039;] &#061; 0;<br \/>\n  statusStats[&#039;3xx&#039;] &#061; 0;<br \/>\n  statusStats[&#039;4xx&#039;] &#061; 0;<br \/>\n  statusStats[&#039;5xx&#039;] &#061; 0;<br \/>\n  statusStats.details &#061; {};<\/p>\n<p>  broadcast({<br \/>\n    type: &#039;STATS_RESET&#039;,<br \/>\n    data: { timestamp: new Date().toISOString() }<br \/>\n  });<br \/>\n}, 60 * 60 * 1000);<\/p>\n<h4>31.6 \u6e10\u8fdb\u5f0fWeb\u5e94\u7528&#xff08;PWA&#xff09;\u4e2d\u7684\u72b6\u6001\u7801<\/h4>\n<h5>31.6.1 \u79bb\u7ebf\u72b6\u6001\u5904\u7406<\/h5>\n<p>javascript<\/p>\n<p>\/\/ \u79bb\u7ebf\u68c0\u6d4b\u4e0e\u670d\u52a1\u72b6\u6001\u7ba1\u7406<br \/>\nclass NetworkStatus {<br \/>\n  constructor() {<br \/>\n    this.isOnline &#061; navigator.onLine;<br \/>\n    this.isServerReachable &#061; true;<br \/>\n    this.lastCheck &#061; null;<br \/>\n    this.listeners &#061; [];<\/p>\n<p>    this.init();<br \/>\n  }<\/p>\n<p>  init() {<br \/>\n    \/\/ \u76d1\u542c\u7f51\u7edc\u72b6\u6001\u53d8\u5316<br \/>\n    window.addEventListener(&#039;online&#039;, () &#061;&gt; this.handleOnline());<br \/>\n    window.addEventListener(&#039;offline&#039;, () &#061;&gt; this.handleOffline());<\/p>\n<p>    \/\/ \u5b9a\u671f\u68c0\u67e5\u670d\u52a1\u5668\u53ef\u7528\u6027<br \/>\n    setInterval(() &#061;&gt; this.checkServerHealth(), 30000);<br \/>\n  }<\/p>\n<p>  async checkServerHealth() {<br \/>\n    try {<br \/>\n      const controller &#061; new AbortController();<br \/>\n      const timeoutId &#061; setTimeout(() &#061;&gt; controller.abort(), 5000);<\/p>\n<p>      const response &#061; await fetch(&#039;\/health&#039;, {<br \/>\n        method: &#039;HEAD&#039;,<br \/>\n        signal: controller.signal,<br \/>\n        cache: &#039;no-store&#039;<br \/>\n      });<\/p>\n<p>      clearTimeout(timeoutId);<\/p>\n<p>      this.isServerReachable &#061; response.ok;<br \/>\n      this.lastCheck &#061; new Date();<\/p>\n<p>      this.notifyListeners();<br \/>\n    } catch (error) {<br \/>\n      this.isServerReachable &#061; false;<br \/>\n      this.lastCheck &#061; new Date();<br \/>\n      this.notifyListeners();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  handleOnline() {<br \/>\n    this.isOnline &#061; true;<br \/>\n    this.notifyListeners();<\/p>\n<p>    \/\/ \u5728\u7ebf\u65f6\u7acb\u5373\u68c0\u67e5\u670d\u52a1\u5668<br \/>\n    this.checkServerHealth();<\/p>\n<p>    \/\/ \u540c\u6b65\u79bb\u7ebf\u671f\u95f4\u7684\u64cd\u4f5c<br \/>\n    this.syncOfflineActions();<br \/>\n  }<\/p>\n<p>  handleOffline() {<br \/>\n    this.isOnline &#061; false;<br \/>\n    this.notifyListeners();<\/p>\n<p>    \/\/ \u663e\u793a\u79bb\u7ebf\u63d0\u793a<br \/>\n    this.showOfflineNotification();<br \/>\n  }<\/p>\n<p>  async syncOfflineActions() {<br \/>\n    const offlineActions &#061; this.getOfflineActions();<\/p>\n<p>    for (const action of offlineActions) {<br \/>\n      try {<br \/>\n        await this.retryAction(action);<br \/>\n        this.removeOfflineAction(action.id);<br \/>\n      } catch (error) {<br \/>\n        console.error(&#039;Failed to sync action:&#039;, action, error);<br \/>\n      }<br \/>\n    }<br \/>\n  }<\/p>\n<p>  getOfflineActions() {<br \/>\n    const actions &#061; localStorage.getItem(&#039;offline_actions&#039;);<br \/>\n    return actions ? JSON.parse(actions) : [];<br \/>\n  }<\/p>\n<p>  saveOfflineAction(action) {<br \/>\n    const actions &#061; this.getOfflineActions();<br \/>\n    actions.push({<br \/>\n      &#8230;action,<br \/>\n      id: Date.now().toString(),<br \/>\n      timestamp: new Date().toISOString()<br \/>\n    });<br \/>\n    localStorage.setItem(&#039;offline_actions&#039;, JSON.stringify(actions));<br \/>\n  }<\/p>\n<p>  removeOfflineAction(id) {<br \/>\n    const actions &#061; this.getOfflineActions();<br \/>\n    const filtered &#061; actions.filter(a &#061;&gt; a.id !&#061;&#061; id);<br \/>\n    localStorage.setItem(&#039;offline_actions&#039;, JSON.stringify(filtered));<br \/>\n  }<\/p>\n<p>  async retryAction(action) {<br \/>\n    \/\/ \u6839\u636eaction\u7c7b\u578b\u91cd\u8bd5\u8bf7\u6c42<br \/>\n    const { type, data, url, method } &#061; action;<\/p>\n<p>    const response &#061; await fetch(url, {<br \/>\n      method,<br \/>\n      headers: {<br \/>\n        &#039;Content-Type&#039;: &#039;application\/json&#039;,<br \/>\n        &#039;X-Offline-Retry&#039;: &#039;true&#039;,<br \/>\n        &#039;X-Original-Timestamp&#039;: data.timestamp<br \/>\n      },<br \/>\n      body: JSON.stringify(data)<br \/>\n    });<\/p>\n<p>    if (!response.ok) {<br \/>\n      throw new Error(&#096;Retry failed: ${response.status}&#096;);<br \/>\n    }<\/p>\n<p>    return response.json();<br \/>\n  }<\/p>\n<p>  addListener(callback) {<br \/>\n    this.listeners.push(callback);<br \/>\n  }<\/p>\n<p>  removeListener(callback) {<br \/>\n    this.listeners &#061; this.listeners.filter(l &#061;&gt; l !&#061;&#061; callback);<br \/>\n  }<\/p>\n<p>  notifyListeners() {<br \/>\n    const status &#061; this.getStatus();<br \/>\n    this.listeners.forEach(callback &#061;&gt; callback(status));<br \/>\n  }<\/p>\n<p>  getStatus() {<br \/>\n    return {<br \/>\n      isOnline: this.isOnline,<br \/>\n      isServerReachable: this.isServerReachable,<br \/>\n      lastCheck: this.lastCheck,<br \/>\n      canPerformAction: this.isOnline &amp;&amp; this.isServerReachable,<br \/>\n      isOffline: !this.isOnline,<br \/>\n      isUnreachable: this.isOnline &amp;&amp; !this.isServerReachable<br \/>\n    };<br \/>\n  }<\/p>\n<p>  showOfflineNotification() {<br \/>\n    \/\/ \u663e\u793a\u79bb\u7ebf\u901a\u77e5<br \/>\n    if (&#039;Notification&#039; in window &amp;&amp; Notification.permission &#061;&#061;&#061; &#039;granted&#039;) {<br \/>\n      new Notification(&#039;\u5e94\u7528\u5df2\u79bb\u7ebf&#039;, {<br \/>\n        body: &#039;\u60a8\u5f53\u524d\u5904\u4e8e\u79bb\u7ebf\u72b6\u6001&#xff0c;\u67d0\u4e9b\u529f\u80fd\u53ef\u80fd\u53d7\u9650&#039;,<br \/>\n        icon: &#039;\/icons\/offline.png&#039;,<br \/>\n        tag: &#039;network-status&#039;<br \/>\n      });<br \/>\n    }<\/p>\n<p>    \/\/ \u66f4\u65b0UI\u72b6\u6001<br \/>\n    document.dispatchEvent(new CustomEvent(&#039;networkStatusChange&#039;, {<br \/>\n      detail: this.getStatus()<br \/>\n    }));<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ \u4f7f\u7528\u793a\u4f8b<br \/>\nconst networkStatus &#061; new NetworkStatus();<\/p>\n<p>\/\/ \u5728\u9700\u8981\u7f51\u7edc\u8bf7\u6c42\u7684\u5730\u65b9<br \/>\nasync function submitForm(data) {<br \/>\n  const status &#061; networkStatus.getStatus();<\/p>\n<p>  if (!status.canPerformAction) {<br \/>\n    \/\/ \u4fdd\u5b58\u5230\u79bb\u7ebf\u961f\u5217<br \/>\n    networkStatus.saveOfflineAction({<br \/>\n      type: &#039;form_submission&#039;,<br \/>\n      url: &#039;\/api\/submit&#039;,<br \/>\n      method: &#039;POST&#039;,<br \/>\n      data<br \/>\n    });<\/p>\n<p>    \/\/ \u663e\u793a\u79bb\u7ebf\u63d0\u793a<br \/>\n    return {<br \/>\n      success: false,<br \/>\n      offline: true,<br \/>\n      message: &#039;\u5f53\u524d\u79bb\u7ebf&#xff0c;\u6570\u636e\u5df2\u4fdd\u5b58&#xff0c;\u5c06\u5728\u6062\u590d\u7f51\u7edc\u540e\u63d0\u4ea4&#039;<br \/>\n    };<br \/>\n  }<\/p>\n<p>  try {<br \/>\n    const response &#061; await fetch(&#039;\/api\/submit&#039;, {<br \/>\n      method: &#039;POST&#039;,<br \/>\n      headers: {<br \/>\n        &#039;Content-Type&#039;: &#039;application\/json&#039;<br \/>\n      },<br \/>\n      body: JSON.stringify(data)<br \/>\n    });<\/p>\n<p>    if (!response.ok) {<br \/>\n      throw new Error(&#096;Submission failed: ${response.status}&#096;);<br \/>\n    }<\/p>\n<p>    return response.json();<br \/>\n  } catch (error) {<br \/>\n    if (!navigator.onLine) {<br \/>\n      \/\/ \u7f51\u7edc\u9519\u8bef&#xff0c;\u4fdd\u5b58\u5230\u79bb\u7ebf\u961f\u5217<br \/>\n      networkStatus.saveOfflineAction({<br \/>\n        type: &#039;form_submission&#039;,<br \/>\n        url: &#039;\/api\/submit&#039;,<br \/>\n        method: &#039;POST&#039;,<br \/>\n        data<br \/>\n      });<\/p>\n<p>      return {<br \/>\n        success: false,<br \/>\n        offline: true,<br \/>\n        message: &#039;\u7f51\u7edc\u8fde\u63a5\u5931\u8d25&#xff0c;\u6570\u636e\u5df2\u4fdd\u5b58\u5230\u672c\u5730&#039;<br \/>\n      };<br \/>\n    }<\/p>\n<p>    throw error;<br \/>\n  }<br \/>\n}<\/p>\n<h4>31.7 \u6d4b\u8bd5\u7b56\u7565<\/h4>\n<h5>31.7.1 \u72b6\u6001\u7801\u6d4b\u8bd5\u5957\u4ef6<\/h5>\n<p>javascript<\/p>\n<p>\/\/ Jest\u72b6\u6001\u7801\u6d4b\u8bd5\u793a\u4f8b<br \/>\ndescribe(&#039;API Status Codes&#039;, () &#061;&gt; {<br \/>\n  describe(&#039;GET \/api\/users&#039;, () &#061;&gt; {<br \/>\n    test(&#039;should return 200 with user list for authenticated admin&#039;, async () &#061;&gt; {<br \/>\n      const response &#061; await request(app)<br \/>\n        .get(&#039;\/api\/users&#039;)<br \/>\n        .set(&#039;Authorization&#039;, &#096;Bearer ${adminToken}&#096;);<\/p>\n<p>      expect(response.status).toBe(200);<br \/>\n      expect(response.body).toHaveProperty(&#039;data&#039;);<br \/>\n      expect(Array.isArray(response.body.data)).toBe(true);<br \/>\n    });<\/p>\n<p>    test(&#039;should return 401 for unauthenticated requests&#039;, async () &#061;&gt; {<br \/>\n      const response &#061; await request(app)<br \/>\n        .get(&#039;\/api\/users&#039;);<\/p>\n<p>      expect(response.status).toBe(401);<br \/>\n      expect(response.body.error.code).toBe(&#039;UNAUTHORIZED&#039;);<br \/>\n    });<\/p>\n<p>    test(&#039;should return 403 for non-admin users&#039;, async () &#061;&gt; {<br \/>\n      const response &#061; await request(app)<br \/>\n        .get(&#039;\/api\/users&#039;)<br \/>\n        .set(&#039;Authorization&#039;, &#096;Bearer ${userToken}&#096;);<\/p>\n<p>      expect(response.status).toBe(403);<br \/>\n      expect(response.body.error.code).toBe(&#039;INSUFFICIENT_PERMISSIONS&#039;);<br \/>\n    });<br \/>\n  });<\/p>\n<p>  describe(&#039;POST \/api\/users&#039;, () &#061;&gt; {<br \/>\n    test(&#039;should return 201 on successful creation&#039;, async () &#061;&gt; {<br \/>\n      const userData &#061; {<br \/>\n        name: &#039;Test User&#039;,<br \/>\n        email: &#039;test&#064;example.com&#039;,<br \/>\n        password: &#039;securepassword123&#039;<br \/>\n      };<\/p>\n<p>      const response &#061; await request(app)<br \/>\n        .post(&#039;\/api\/users&#039;)<br \/>\n        .send(userData);<\/p>\n<p>      expect(response.status).toBe(201);<br \/>\n      expect(response.headers.location).toMatch(\/\\\\\/api\\\\\/users\\\\\/\\\\w&#043;\/);<br \/>\n      expect(response.body).toHaveProperty(&#039;id&#039;);<br \/>\n      expect(response.body.email).toBe(userData.email);<br \/>\n    });<\/p>\n<p>    test(&#039;should return 400 for invalid email&#039;, async () &#061;&gt; {<br \/>\n      const userData &#061; {<br \/>\n        name: &#039;Test User&#039;,<br \/>\n        email: &#039;invalid-email&#039;,<br \/>\n        password: &#039;password123&#039;<br \/>\n      };<\/p>\n<p>      const response &#061; await request(app)<br \/>\n        .post(&#039;\/api\/users&#039;)<br \/>\n        .send(userData);<\/p>\n<p>      expect(response.status).toBe(400);<br \/>\n      expect(response.body.error.code).toBe(&#039;VALIDATION_ERROR&#039;);<br \/>\n      expect(response.body.error.details[0].field).toBe(&#039;email&#039;);<br \/>\n    });<\/p>\n<p>    test(&#039;should return 409 for duplicate email&#039;, async () &#061;&gt; {<br \/>\n      \/\/ \u5148\u521b\u5efa\u4e00\u4e2a\u7528\u6237<br \/>\n      await request(app)<br \/>\n        .post(&#039;\/api\/users&#039;)<br \/>\n        .send({<br \/>\n          name: &#039;Existing User&#039;,<br \/>\n          email: &#039;duplicate&#064;example.com&#039;,<br \/>\n          password: &#039;password123&#039;<br \/>\n        });<\/p>\n<p>      \/\/ \u5c1d\u8bd5\u7528\u76f8\u540c\u90ae\u7bb1\u521b\u5efa<br \/>\n      const response &#061; await request(app)<br \/>\n        .post(&#039;\/api\/users&#039;)<br \/>\n        .send({<br \/>\n          name: &#039;New User&#039;,<br \/>\n          email: &#039;duplicate&#064;example.com&#039;,<br \/>\n          password: &#039;newpassword123&#039;<br \/>\n        });<\/p>\n<p>      expect(response.status).toBe(409);<br \/>\n      expect(response.body.error.code).toBe(&#039;DUPLICATE_EMAIL&#039;);<br \/>\n    });<br \/>\n  });<\/p>\n<p>  describe(&#039;Error handling&#039;, () &#061;&gt; {<br \/>\n    test(&#039;should return 500 for unhandled exceptions&#039;, async () &#061;&gt; {<br \/>\n      \/\/ \u6a21\u62df\u6570\u636e\u5e93\u9519\u8bef<br \/>\n      jest.spyOn(UserModel, &#039;findAll&#039;).mockImplementation(() &#061;&gt; {<br \/>\n        throw new Error(&#039;Database connection failed&#039;);<br \/>\n      });<\/p>\n<p>      const response &#061; await request(app)<br \/>\n        .get(&#039;\/api\/users&#039;)<br \/>\n        .set(&#039;Authorization&#039;, &#096;Bearer ${adminToken}&#096;);<\/p>\n<p>      expect(response.status).toBe(500);<br \/>\n      expect(response.body.error.code).toBe(&#039;INTERNAL_SERVER_ERROR&#039;);<\/p>\n<p>      \/\/ \u9a8c\u8bc1\u9519\u8bef\u88ab\u8bb0\u5f55<br \/>\n      expect(logger.error).toHaveBeenCalled();<br \/>\n    });<\/p>\n<p>    test(&#039;should return 503 during maintenance&#039;, async () &#061;&gt; {<br \/>\n      \/\/ \u542f\u7528\u7ef4\u62a4\u6a21\u5f0f<br \/>\n      process.env.MAINTENANCE_MODE &#061; &#039;true&#039;;<\/p>\n<p>      const response &#061; await request(app)<br \/>\n        .get(&#039;\/api\/users&#039;)<br \/>\n        .set(&#039;Authorization&#039;, &#096;Bearer ${adminToken}&#096;);<\/p>\n<p>      expect(response.status).toBe(503);<br \/>\n      expect(response.headers[&#039;retry-after&#039;]).toBeDefined();<br \/>\n      expect(response.body.error.code).toBe(&#039;MAINTENANCE_MODE&#039;);<\/p>\n<p>      \/\/ \u6e05\u7406<br \/>\n      delete process.env.MAINTENANCE_MODE;<br \/>\n    });<br \/>\n  });<br \/>\n});<\/p>\n<p>\/\/ E2E\u6d4b\u8bd5 &#8211; Cypress<br \/>\ndescribe(&#039;Status Code E2E Tests&#039;, () &#061;&gt; {<br \/>\n  beforeEach(() &#061;&gt; {<br \/>\n    cy.intercept(&#039;GET&#039;, &#039;\/api\/user\/profile&#039;).as(&#039;getProfile&#039;);<br \/>\n  });<\/p>\n<p>  it(&#039;should handle 401 gracefully&#039;, () &#061;&gt; {<br \/>\n    \/\/ \u6a21\u62df401\u54cd\u5e94<br \/>\n    cy.intercept(&#039;GET&#039;, &#039;\/api\/user\/profile&#039;, {<br \/>\n      statusCode: 401,<br \/>\n      body: {<br \/>\n        error: {<br \/>\n          code: &#039;UNAUTHORIZED&#039;,<br \/>\n          message: &#039;Authentication required&#039;<br \/>\n        }<br \/>\n      }<br \/>\n    }).as(&#039;unauthorized&#039;);<\/p>\n<p>    cy.visit(&#039;\/profile&#039;);<br \/>\n    cy.wait(&#039;&#064;unauthorized&#039;);<\/p>\n<p>    \/\/ \u9a8c\u8bc1\u88ab\u91cd\u5b9a\u5411\u5230\u767b\u5f55\u9875<br \/>\n    cy.url().should(&#039;include&#039;, &#039;\/login&#039;);<br \/>\n    cy.get(&#039;.error-message&#039;).should(&#039;contain&#039;, &#039;\u8bf7\u91cd\u65b0\u767b\u5f55&#039;);<br \/>\n  });<\/p>\n<p>  it(&#039;should handle 404 gracefully&#039;, () &#061;&gt; {<br \/>\n    cy.visit(&#039;\/nonexistent-page&#039;);<\/p>\n<p>    \/\/ \u9a8c\u8bc1\u663e\u793a404\u9875\u9762<br \/>\n    cy.get(&#039;h1&#039;).should(&#039;contain&#039;, &#039;404&#039;);<br \/>\n    cy.get(&#039;a&#039;).contains(&#039;\u8fd4\u56de\u9996\u9875&#039;).should(&#039;be.visible&#039;);<br \/>\n  });<\/p>\n<p>  it(&#039;should retry on 503&#039;, () &#061;&gt; {<br \/>\n    let requestCount &#061; 0;<\/p>\n<p>    cy.intercept(&#039;GET&#039;, &#039;\/api\/data&#039;, (req) &#061;&gt; {<br \/>\n      requestCount&#043;&#043;;<\/p>\n<p>      if (requestCount &#061;&#061;&#061; 1) {<br \/>\n        req.reply({<br \/>\n          statusCode: 503,<br \/>\n          headers: {<br \/>\n            &#039;retry-after&#039;: &#039;1&#039;<br \/>\n          }<br \/>\n        });<br \/>\n      } else {<br \/>\n        req.reply({<br \/>\n          statusCode: 200,<br \/>\n          body: { data: &#039;success&#039; }<br \/>\n        });<br \/>\n      }<br \/>\n    }).as(&#039;getData&#039;);<\/p>\n<p>    cy.visit(&#039;\/dashboard&#039;);<br \/>\n    cy.wait(&#039;&#064;getData&#039;);<\/p>\n<p>    \/\/ \u9a8c\u8bc1\u6700\u7ec8\u6210\u529f<br \/>\n    cy.get(&#039;.data-content&#039;).should(&#039;contain&#039;, &#039;success&#039;);<br \/>\n  });<br \/>\n});<\/p>\n<h4>31.8 \u6027\u80fd\u76d1\u63a7\u4e0e\u544a\u8b66<\/h4>\n<h5>31.8.1 \u57fa\u4e8e\u72b6\u6001\u7801\u7684\u76d1\u63a7\u6307\u6807<\/h5>\n<p>yaml<\/p>\n<p># Prometheus\u89c4\u5219\u914d\u7f6e<br \/>\ngroups:<br \/>\n  &#8211; name: http_status_alerts<br \/>\n    rules:<br \/>\n      # \u9ad8\u9519\u8bef\u7387\u544a\u8b66<br \/>\n      &#8211; alert: HighErrorRate<br \/>\n        expr: |<br \/>\n          rate(http_requests_total{status&#061;~&#034;5..&#034;}[5m]) \/<br \/>\n          rate(http_requests_total[5m]) &gt; 0.05<br \/>\n        for: 5m<br \/>\n        labels:<br \/>\n          severity: critical<br \/>\n        annotations:<br \/>\n          summary: &#034;High 5xx error rate detected&#034;<br \/>\n          description: |<br \/>\n            Error rate is {{ $value }}% for endpoint {{ $labels.endpoint }}.<br \/>\n            This exceeds the 5% threshold.<\/p>\n<p>      # 4xx\u9519\u8bef\u7387\u589e\u52a0<br \/>\n      &#8211; alert: HighClientErrorRate<br \/>\n        expr: |<br \/>\n          rate(http_requests_total{status&#061;~&#034;4..&#034;}[5m]) \/<br \/>\n          rate(http_requests_total[5m]) &gt; 0.10<br \/>\n        for: 10m<br \/>\n        labels:<br \/>\n          severity: warning<br \/>\n        annotations:<br \/>\n          summary: &#034;High 4xx error rate detected&#034;<br \/>\n          description: |<br \/>\n            Client error rate is {{ $value }}% for endpoint {{ $labels.endpoint }}.<br \/>\n            This may indicate issues with client requests.<\/p>\n<p>      # \u7279\u5b9a\u7aef\u70b9\u4e0d\u53ef\u7528<br \/>\n      &#8211; alert: EndpointDown<br \/>\n        expr: |<br \/>\n          sum(rate(http_requests_total{endpoint&#061;&#034;\/api\/health&#034;}[5m])) &#061;&#061; 0<br \/>\n        for: 2m<br \/>\n        labels:<br \/>\n          severity: critical<br \/>\n        annotations:<br \/>\n          summary: &#034;Health endpoint is down&#034;<br \/>\n          description: &#034;The health endpoint has not received any requests in the last 2 minutes.&#034;<\/p>\n<p>      # \u54cd\u5e94\u65f6\u95f4\u589e\u52a0\u4e0e\u72b6\u6001\u7801\u5173\u8054<br \/>\n      &#8211; alert: SlowResponsesWithErrors<br \/>\n        expr: |<br \/>\n          histogram_quantile(0.95,<br \/>\n            rate(http_request_duration_seconds_bucket{status&#061;~&#034;5..&#034;}[5m])<br \/>\n          ) &gt; 2<br \/>\n        for: 5m<br \/>\n        labels:<br \/>\n          severity: warning<br \/>\n        annotations:<br \/>\n          summary: &#034;Slow responses with server errors&#034;<br \/>\n          description: |<br \/>\n            95th percentile response time for error responses is {{ $value }}s.<br \/>\n            Slow responses with errors may indicate deeper issues.<\/p>\n<h5>31.8.2 \u5206\u5e03\u5f0f\u8ffd\u8e2a\u4e2d\u7684\u72b6\u6001\u7801<\/h5>\n<p>javascript<\/p>\n<p>\/\/ OpenTelemetry\u8ffd\u8e2a\u914d\u7f6e<br \/>\nconst { NodeTracerProvider } &#061; require(&#039;&#064;opentelemetry\/node&#039;);<br \/>\nconst { SimpleSpanProcessor } &#061; require(&#039;&#064;opentelemetry\/tracing&#039;);<br \/>\nconst { JaegerExporter } &#061; require(&#039;&#064;opentelemetry\/exporter-jaeger&#039;);<br \/>\nconst { ZipkinExporter } &#061; require(&#039;&#064;opentelemetry\/exporter-zipkin&#039;);<\/p>\n<p>const tracerProvider &#061; new NodeTracerProvider();<br \/>\ntracerProvider.register();<\/p>\n<p>\/\/ Jaeger\u5bfc\u51fa\u5668<br \/>\nconst jaegerExporter &#061; new JaegerExporter({<br \/>\n  serviceName: &#039;api-service&#039;,<br \/>\n  tags: [<br \/>\n    { key: &#039;environment&#039;, value: process.env.NODE_ENV }<br \/>\n  ]<br \/>\n});<\/p>\n<p>\/\/ \u6dfb\u52a0\u8de8\u5ea6\u5904\u7406\u5668<br \/>\ntracerProvider.addSpanProcessor(<br \/>\n  new SimpleSpanProcessor(jaegerExporter)<br \/>\n);<\/p>\n<p>\/\/ Express\u4e2d\u95f4\u4ef6<br \/>\nconst { expressMiddleware } &#061; require(&#039;&#064;opentelemetry\/express&#039;);<\/p>\n<p>app.use(expressMiddleware(&#039;api-service&#039;));<\/p>\n<p>\/\/ \u81ea\u5b9a\u4e49\u8de8\u5ea6\u5c5e\u6027<br \/>\napp.use((req, res, next) &#061;&gt; {<br \/>\n  const span &#061; tracerProvider.getTracer(&#039;api&#039;).getCurrentSpan();<\/p>\n<p>  if (span) {<br \/>\n    \/\/ \u6dfb\u52a0\u8bf7\u6c42\u4fe1\u606f<br \/>\n    span.setAttribute(&#039;http.method&#039;, req.method);<br \/>\n    span.setAttribute(&#039;http.url&#039;, req.url);<br \/>\n    span.setAttribute(&#039;http.user_agent&#039;, req.get(&#039;User-Agent&#039;));<\/p>\n<p>    \/\/ \u54cd\u5e94\u5b8c\u6210\u540e\u6dfb\u52a0\u72b6\u6001\u7801<br \/>\n    const originalEnd &#061; res.end;<br \/>\n    res.end &#061; function(&#8230;args) {<br \/>\n      span.setAttribute(&#039;http.status_code&#039;, res.statusCode);<br \/>\n      span.setAttribute(&#039;http.status_text&#039;, res.statusMessage);<\/p>\n<p>      \/\/ \u9519\u8bef\u76f8\u5173\u5c5e\u6027<br \/>\n      if (res.statusCode &gt;&#061; 400) {<br \/>\n        span.setAttribute(&#039;error&#039;, true);<br \/>\n        span.setAttribute(&#039;error.type&#039;, &#096;HTTP_${res.statusCode}&#096;);<\/p>\n<p>        if (res.locals.error) {<br \/>\n          span.setAttribute(&#039;error.message&#039;, res.locals.error.message);<br \/>\n          span.setAttribute(&#039;error.stack&#039;, res.locals.error.stack);<br \/>\n        }<br \/>\n      }<\/p>\n<p>      originalEnd.apply(this, args);<br \/>\n    };<br \/>\n  }<\/p>\n<p>  next();<br \/>\n});<\/p>\n<p>\/\/ \u4e1a\u52a1\u903b\u8f91\u4e2d\u7684\u8ffd\u8e2a<br \/>\nasync function processOrder(orderId) {<br \/>\n  const tracer &#061; tracerProvider.getTracer(&#039;order-service&#039;);<br \/>\n  const span &#061; tracer.startSpan(&#039;processOrder&#039;, {<br \/>\n    attributes: {<br \/>\n      &#039;order.id&#039;: orderId,<br \/>\n      &#039;component&#039;: &#039;order-processor&#039;<br \/>\n    }<br \/>\n  });<\/p>\n<p>  try {<br \/>\n    const order &#061; await fetchOrder(orderId);<br \/>\n    span.addEvent(&#039;order_fetched&#039;);<\/p>\n<p>    if (!order) {<br \/>\n      span.setAttribute(&#039;error&#039;, true);<br \/>\n      span.setAttribute(&#039;error.type&#039;, &#039;ORDER_NOT_FOUND&#039;);<br \/>\n      throw new Error(&#039;Order not found&#039;);<br \/>\n    }<\/p>\n<p>    const result &#061; await validateOrder(order);<br \/>\n    span.addEvent(&#039;order_validated&#039;);<\/p>\n<p>    return result;<br \/>\n  } catch (error) {<br \/>\n    \/\/ \u8bb0\u5f55\u9519\u8bef<br \/>\n    span.recordException(error);<br \/>\n    span.setStatus({<br \/>\n      code: 2, \/\/ ERROR<br \/>\n      message: error.message<br \/>\n    });<\/p>\n<p>    throw error;<br \/>\n  } finally {<br \/>\n    span.end();<br \/>\n  }<br \/>\n}<\/p>\n<h4>31.9 \u56fd\u9645\u5316\u4e0e\u672c\u5730\u5316<\/h4>\n<h5>31.9.1 \u591a\u8bed\u8a00\u9519\u8bef\u6d88\u606f<\/h5>\n<p>javascript<\/p>\n<p>\/\/ \u9519\u8bef\u6d88\u606f\u56fd\u9645\u5316<br \/>\nclass LocalizedError extends Error {<br \/>\n  constructor(messageKey, params &#061; {}, statusCode &#061; 500) {<br \/>\n    super(messageKey);<br \/>\n    this.name &#061; &#039;LocalizedError&#039;;<br \/>\n    this.messageKey &#061; messageKey;<br \/>\n    this.params &#061; params;<br \/>\n    this.statusCode &#061; statusCode;<br \/>\n    this.timestamp &#061; new Date();<br \/>\n  }<\/p>\n<p>  getLocalizedMessage(locale &#061; &#039;zh-CN&#039;) {<br \/>\n    const messages &#061; {<br \/>\n      &#039;zh-CN&#039;: {<br \/>\n        &#039;USER_NOT_FOUND&#039;: &#039;\u7528\u6237\u4e0d\u5b58\u5728&#039;,<br \/>\n        &#039;INVALID_CREDENTIALS&#039;: &#039;\u7528\u6237\u540d\u6216\u5bc6\u7801\u9519\u8bef&#039;,<br \/>\n        &#039;EMAIL_ALREADY_EXISTS&#039;: &#039;\u90ae\u7bb1\u5df2\u88ab\u6ce8\u518c&#039;,<br \/>\n        &#039;PERMISSION_DENIED&#039;: &#039;\u6743\u9650\u4e0d\u8db3&#039;,<br \/>\n        &#039;NETWORK_ERROR&#039;: &#039;\u7f51\u7edc\u8fde\u63a5\u5931\u8d25&#xff0c;\u8bf7\u68c0\u67e5\u7f51\u7edc\u8bbe\u7f6e&#039;,<br \/>\n        &#039;SERVER_ERROR&#039;: &#039;\u670d\u52a1\u5668\u9519\u8bef&#xff0c;\u8bf7\u7a0d\u540e\u91cd\u8bd5&#039;,<br \/>\n        &#039;VALIDATION_ERROR&#039;: &#039;\u8f93\u5165\u9a8c\u8bc1\u5931\u8d25&#039;,<br \/>\n        &#039;RATE_LIMITED&#039;: &#039;\u8bf7\u6c42\u8fc7\u4e8e\u9891\u7e41&#xff0c;\u8bf7\u7a0d\u540e\u91cd\u8bd5&#039;,<br \/>\n        &#039;MAINTENANCE_MODE&#039;: &#039;\u7cfb\u7edf\u7ef4\u62a4\u4e2d&#xff0c;\u8bf7\u7a0d\u540e\u8bbf\u95ee&#039;<br \/>\n      },<br \/>\n      &#039;en-US&#039;: {<br \/>\n        &#039;USER_NOT_FOUND&#039;: &#039;User not found&#039;,<br \/>\n        &#039;INVALID_CREDENTIALS&#039;: &#039;Invalid username or password&#039;,<br \/>\n        &#039;EMAIL_ALREADY_EXISTS&#039;: &#039;Email already exists&#039;,<br \/>\n        &#039;PERMISSION_DENIED&#039;: &#039;Permission denied&#039;,<br \/>\n        &#039;NETWORK_ERROR&#039;: &#039;Network connection failed&#039;,<br \/>\n        &#039;SERVER_ERROR&#039;: &#039;Server error, please try again later&#039;,<br \/>\n        &#039;VALIDATION_ERROR&#039;: &#039;Validation failed&#039;,<br \/>\n        &#039;RATE_LIMITED&#039;: &#039;Too many requests, please try again later&#039;,<br \/>\n        &#039;MAINTENANCE_MODE&#039;: &#039;System under maintenance&#039;<br \/>\n      },<br \/>\n      &#039;ja-JP&#039;: {<br \/>\n        &#039;USER_NOT_FOUND&#039;: &#039;\u30e6\u30fc\u30b6\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093&#039;,<br \/>\n        &#039;INVALID_CREDENTIALS&#039;: &#039;\u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u9593\u9055\u3063\u3066\u3044\u307e\u3059&#039;,<br \/>\n        &#039;EMAIL_ALREADY_EXISTS&#039;: &#039;\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u306f\u65e2\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u307e\u3059&#039;,<br \/>\n        &#039;PERMISSION_DENIED&#039;: &#039;\u6a29\u9650\u304c\u4e0d\u8db3\u3057\u3066\u3044\u307e\u3059&#039;,<br \/>\n        &#039;NETWORK_ERROR&#039;: &#039;\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f&#039;,<br \/>\n        &#039;SERVER_ERROR&#039;: &#039;\u30b5\u30fc\u30d0\u30fc\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f&#039;,<br \/>\n        &#039;VALIDATION_ERROR&#039;: &#039;\u5165\u529b\u691c\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f&#039;,<br \/>\n        &#039;RATE_LIMITED&#039;: &#039;\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044&#039;,<br \/>\n        &#039;MAINTENANCE_MODE&#039;: &#039;\u30b7\u30b9\u30c6\u30e0\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u4e2d\u3067\u3059&#039;<br \/>\n      }<br \/>\n    };<\/p>\n<p>    const localeMessages &#061; messages[locale] || messages[&#039;en-US&#039;];<br \/>\n    let message &#061; localeMessages[this.messageKey] || this.messageKey;<\/p>\n<p>    \/\/ \u66ff\u6362\u53c2\u6570<br \/>\n    Object.keys(this.params).forEach(key &#061;&gt; {<br \/>\n      message &#061; message.replace(&#096;{${key}}&#096;, this.params[key]);<br \/>\n    });<\/p>\n<p>    return message;<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ \u4f7f\u7528\u793a\u4f8b<br \/>\napp.get(&#039;\/api\/users\/:id&#039;, async (req, res, next) &#061;&gt; {<br \/>\n  try {<br \/>\n    const user &#061; await User.findById(req.params.id);<\/p>\n<p>    if (!user) {<br \/>\n      throw new LocalizedError(&#039;USER_NOT_FOUND&#039;,<br \/>\n        { userId: req.params.id },<br \/>\n        404<br \/>\n      );<br \/>\n    }<\/p>\n<p>    res.json(user);<br \/>\n  } catch (error) {<br \/>\n    next(error);<br \/>\n  }<br \/>\n});<\/p>\n<p>\/\/ \u9519\u8bef\u5904\u7406\u4e2d\u95f4\u4ef6<br \/>\napp.use((error, req, res, next) &#061;&gt; {<br \/>\n  \/\/ \u83b7\u53d6\u5ba2\u6237\u7aef\u9996\u9009\u8bed\u8a00<br \/>\n  const acceptLanguage &#061; req.get(&#039;Accept-Language&#039;);<br \/>\n  const locale &#061; acceptLanguage?.split(&#039;,&#039;)[0] || &#039;zh-CN&#039;;<\/p>\n<p>  if (error instanceof LocalizedError) {<br \/>\n    return res.status(error.statusCode).json({<br \/>\n      error: {<br \/>\n        code: error.messageKey,<br \/>\n        message: error.getLocalizedMessage(locale),<br \/>\n        params: error.params,<br \/>\n        timestamp: error.timestamp.toISOString(),<br \/>\n        documentation: &#096;https:\/\/api.example.com\/docs\/errors\/${error.messageKey}&#096;<br \/>\n      }<br \/>\n    });<br \/>\n  }<\/p>\n<p>  \/\/ \u5904\u7406\u5176\u4ed6\u9519\u8bef<br \/>\n  const isProduction &#061; process.env.NODE_ENV &#061;&#061;&#061; &#039;production&#039;;<\/p>\n<p>  res.status(error.statusCode || 500).json({<br \/>\n    error: {<br \/>\n      code: &#039;INTERNAL_ERROR&#039;,<br \/>\n      message: isProduction<br \/>\n        ? new LocalizedError(&#039;SERVER_ERROR&#039;).getLocalizedMessage(locale)<br \/>\n        : error.message,<br \/>\n      &#8230;(!isProduction &amp;&amp; { stack: error.stack }),<br \/>\n      timestamp: new Date().toISOString()<br \/>\n    }<br \/>\n  });<br \/>\n});<\/p>\n<h4>31.10 \u603b\u7ed3\u4e0e\u6700\u4f73\u5b9e\u8df5\u6e05\u5355<\/h4>\n<h5>31.10.1 Web\u5f00\u53d1\u72b6\u6001\u7801\u6700\u4f73\u5b9e\u8df5\u603b\u7ed3<\/h5>\n<li>\n<p>\u8bed\u4e49\u6b63\u786e\u6027<\/p>\n<ul>\n<li>\n<p>\u4e3a\u6bcf\u79cd\u60c5\u51b5\u9009\u62e9\u6700\u51c6\u786e\u7684\u72b6\u6001\u7801<\/p>\n<\/li>\n<li>\n<p>\u907f\u514d\u6ee5\u7528200 OK\u8868\u793a\u6240\u6709\u60c5\u51b5<\/p>\n<\/li>\n<li>\n<p>\u4f7f\u7528422\u4ee3\u66ff400\u8fdb\u884c\u8f93\u5165\u9a8c\u8bc1\u9519\u8bef<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u7528\u6237\u4f53\u9a8c<\/p>\n<ul>\n<li>\n<p>\u63d0\u4f9b\u7528\u6237\u53cb\u597d\u7684\u9519\u8bef\u9875\u9762<\/p>\n<\/li>\n<li>\n<p>\u5305\u542b\u6709\u7528\u7684\u64cd\u4f5c\u5efa\u8bae<\/p>\n<\/li>\n<li>\n<p>\u652f\u6301\u591a\u8bed\u8a00\u9519\u8bef\u6d88\u606f<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u5b89\u5168\u6027<\/p>\n<ul>\n<li>\n<p>\u907f\u514d\u5728\u9519\u8bef\u54cd\u5e94\u4e2d\u6cc4\u9732\u654f\u611f\u4fe1\u606f<\/p>\n<\/li>\n<li>\n<p>\u5b9e\u65bd\u9002\u5f53\u7684\u5b89\u5168\u5934<\/p>\n<\/li>\n<li>\n<p>\u9632\u6b62\u901a\u8fc7\u9519\u8bef\u4fe1\u606f\u8fdb\u884c\u4fe1\u606f\u6536\u96c6<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u53ef\u89c2\u6d4b\u6027<\/p>\n<ul>\n<li>\n<p>\u8bb0\u5f55\u7ed3\u6784\u5316\u7684\u9519\u8bef\u65e5\u5fd7<\/p>\n<\/li>\n<li>\n<p>\u76d1\u63a7\u72b6\u6001\u7801\u5206\u5e03<\/p>\n<\/li>\n<li>\n<p>\u8bbe\u7f6e\u57fa\u4e8e\u72b6\u6001\u7801\u7684\u544a\u8b66<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u6027\u80fd<\/p>\n<ul>\n<li>\n<p>\u5229\u7528\u7f13\u5b58\u76f8\u5173\u72b6\u6001\u7801&#xff08;304&#xff09;<\/p>\n<\/li>\n<li>\n<p>\u5b9e\u73b0\u9002\u5f53\u7684\u91cd\u8bd5\u673a\u5236<\/p>\n<\/li>\n<li>\n<p>\u76d1\u63a7\u6162\u901f\u54cd\u5e94\u4e0e\u9519\u8bef\u7684\u5173\u7cfb<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u517c\u5bb9\u6027<\/p>\n<ul>\n<li>\n<p>\u8003\u8651\u65e7\u5ba2\u6237\u7aef\u517c\u5bb9\u6027<\/p>\n<\/li>\n<li>\n<p>\u63d0\u4f9b\u9002\u5f53\u7684\u964d\u7ea7\u65b9\u6848<\/p>\n<\/li>\n<li>\n<p>\u652f\u6301\u6e10\u8fdb\u5f0f\u589e\u5f3a<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u6d4b\u8bd5<\/p>\n<ul>\n<li>\n<p>\u4e3a\u6240\u6709\u72b6\u6001\u7801\u7f16\u5199\u6d4b\u8bd5<\/p>\n<\/li>\n<li>\n<p>\u5305\u62ec\u9519\u8bef\u573a\u666f\u6d4b\u8bd5<\/p>\n<\/li>\n<li>\n<p>\u5b9e\u65bd\u7aef\u5230\u7aef\u9519\u8bef\u5904\u7406\u6d4b\u8bd5<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<h5>31.10.2 \u5b8c\u6574\u7684\u72b6\u6001\u7801\u5904\u7406\u6d41\u7a0b\u56fe<\/h5>\n<p>text<\/p>\n<p>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510<br \/>\n\u2502   \u6536\u5230\u8bf7\u6c42       \u2502<br \/>\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<br \/>\n         \u2502<br \/>\n         \u25bc<br \/>\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510<br \/>\n\u2502   \u9a8c\u8bc1\u8bf7\u6c42       \u2502<br \/>\n\u2502   &#8211; \u8ba4\u8bc1        \u2502<br \/>\n\u2502   &#8211; \u6388\u6743        \u2502<br \/>\n\u2502   &#8211; \u8f93\u5165\u9a8c\u8bc1    \u2502<br \/>\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<br \/>\n         \u2502<br \/>\n    \u250c\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2510<br \/>\n    \u2502         \u2502<br \/>\n    \u25bc         \u25bc<br \/>\n  401\/403   400\/422<br \/>\n   \u2502         \u2502<br \/>\n   \u2502         \u2502<br \/>\n   \u25bc         \u25bc<br \/>\n \u8fd4\u56de\u9519\u8bef  \u8fd4\u56de\u9519\u8bef<br \/>\n   \u2502         \u2502<br \/>\n   \u2502         \u2502<br \/>\n   \u2514\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518<br \/>\n         \u2502<br \/>\n         \u25bc<br \/>\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510<br \/>\n\u2502   \u5904\u7406\u4e1a\u52a1\u903b\u8f91   \u2502<br \/>\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<br \/>\n         \u2502<br \/>\n    \u250c\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2510<br \/>\n    \u2502         \u2502<br \/>\n    \u25bc         \u25bc<br \/>\n   404       409<br \/>\n   \u2502         \u2502<br \/>\n   \u2502         \u2502<br \/>\n   \u25bc         \u25bc<br \/>\n \u8fd4\u56de\u9519\u8bef  \u8fd4\u56de\u9519\u8bef<br \/>\n   \u2502         \u2502<br \/>\n   \u2502         \u2502<br \/>\n   \u2514\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518<br \/>\n         \u2502<br \/>\n         \u25bc<br \/>\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510<br \/>\n\u2502   \u6267\u884c\u64cd\u4f5c       \u2502<br \/>\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<br \/>\n         \u2502<br \/>\n    \u250c\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2510<br \/>\n    \u2502         \u2502<br \/>\n    \u25bc         \u25bc<br \/>\n   201       200<br \/>\n   \u2502         \u2502<br \/>\n   \u2502         \u2502<br \/>\n   \u25bc         \u25bc<br \/>\n \u521b\u5efa\u6210\u529f  \u64cd\u4f5c\u6210\u529f<br \/>\n   \u2502         \u2502<br \/>\n   \u2502         \u2502<br \/>\n   \u2514\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518<br \/>\n         \u2502<br \/>\n         \u25bc<br \/>\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510<br \/>\n\u2502   \u5f02\u5e38\u5904\u7406       \u2502<br \/>\n\u2502   &#8211; \u8bb0\u5f55\u9519\u8bef    \u2502<br \/>\n\u2502   &#8211; \u8fd4\u56de500     \u2502<br \/>\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<br \/>\n         \u2502<br \/>\n         \u25bc<br \/>\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510<br \/>\n\u2502   \u76d1\u63a7\u4e0e\u544a\u8b66     \u2502<br \/>\n\u2502   &#8211; \u66f4\u65b0\u6307\u6807    \u2502<br \/>\n\u2502   &#8211; \u89e6\u53d1\u544a\u8b66    \u2502<br \/>\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/p>\n<h2>\u7b2c32\u7ae0&#xff1a;\u79fb\u52a8\u5e94\u7528\u4e0e\u72b6\u6001\u7801\u5904\u7406<\/h2>\n<h3>32.1 \u79fb\u52a8\u5e94\u7528\u7f51\u7edc\u73af\u5883\u7684\u7279\u6b8a\u6027<\/h3>\n<h4>32.1.1 \u79fb\u52a8\u7f51\u7edc\u7684\u7279\u70b9\u4e0e\u6311\u6218<\/h4>\n<p>\u79fb\u52a8\u5e94\u7528\u9762\u4e34\u7740\u72ec\u7279\u7684\u7f51\u7edc\u73af\u5883\u6311\u6218&#xff1a;<\/p>\n<li>\n<p>\u7f51\u7edc\u4e0d\u7a33\u5b9a\u6027<\/p>\n<ul>\n<li>\n<p>\u79fb\u52a8\u7f51\u7edc\u5207\u6362&#xff08;4G\/5G\/Wi-Fi&#xff09;<\/p>\n<\/li>\n<li>\n<p>\u4fe1\u53f7\u5f3a\u5f31\u53d8\u5316<\/p>\n<\/li>\n<li>\n<p>\u9891\u7e41\u7684\u65ad\u7ebf\u91cd\u8fde<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u8d44\u6e90\u9650\u5236<\/p>\n<ul>\n<li>\n<p>\u7535\u6c60\u7535\u91cf\u6709\u9650<\/p>\n<\/li>\n<li>\n<p>\u5185\u5b58\u548c\u5b58\u50a8\u9650\u5236<\/p>\n<\/li>\n<li>\n<p>\u6570\u636e\u6d41\u91cf\u6210\u672c<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u7528\u6237\u4f53\u9a8c\u671f\u671b<\/p>\n<ul>\n<li>\n<p>\u5feb\u901f\u54cd\u5e94\u9700\u6c42<\/p>\n<\/li>\n<li>\n<p>\u79bb\u7ebf\u529f\u80fd\u652f\u6301<\/p>\n<\/li>\n<li>\n<p>\u65e0\u7f1d\u7684\u7528\u6237\u4f53\u9a8c<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<h4>32.1.2 \u79fb\u52a8\u5e94\u7528\u7684\u72b6\u6001\u7801\u5904\u7406\u539f\u5219<\/h4>\n<p>swift<\/p>\n<p>\/\/ iOS\u7f51\u7edc\u5c42\u8bbe\u8ba1<br \/>\nclass NetworkManager {<br \/>\n    \/\/ \u79fb\u52a8\u7f51\u7edc\u7279\u6709\u7684\u72b6\u6001\u7801\u5904\u7406<br \/>\n    enum MobileError: Error {<br \/>\n        case networkUnavailable<br \/>\n        case poorConnection<br \/>\n        case rateLimited(retryAfter: TimeInterval)<br \/>\n        case requestTimeout<br \/>\n        case serverUnavailable<br \/>\n        case dataLimitExceeded<br \/>\n    }<\/p>\n<p>    \/\/ \u7f51\u7edc\u72b6\u6001\u76d1\u6d4b<br \/>\n    private let monitor &#061; NWPathMonitor()<br \/>\n    private var currentNetworkType: NetworkType &#061; .unknown<\/p>\n<p>    func setupNetworkMonitoring() {<br \/>\n        monitor.pathUpdateHandler &#061; { [weak self] path in<br \/>\n            if path.status &#061;&#061; .satisfied {<br \/>\n                if path.isExpensive {<br \/>\n                    self?.currentNetworkType &#061; .cellular<br \/>\n                    self?.applyCellularNetworkPolicies()<br \/>\n                } else {<br \/>\n                    self?.currentNetworkType &#061; .wifi<br \/>\n                }<br \/>\n            } else {<br \/>\n                self?.currentNetworkType &#061; .disconnected<br \/>\n            }<br \/>\n        }<br \/>\n        monitor.start(queue: DispatchQueue.global())<br \/>\n    }<\/p>\n<p>    private func applyCellularNetworkPolicies() {<br \/>\n        \/\/ \u8702\u7a9d\u7f51\u7edc\u4e0b\u7684\u7b56\u7565<br \/>\n        \/\/ 1. \u964d\u4f4e\u56fe\u7247\u8d28\u91cf<br \/>\n        \/\/ 2. \u51cf\u5c11\u9884\u52a0\u8f7d<br \/>\n        \/\/ 3. \u538b\u7f29\u6570\u636e<br \/>\n        \/\/ 4. \u9650\u5236\u5927\u6587\u4ef6\u4e0b\u8f7d<br \/>\n    }<br \/>\n}<\/p>\n<h3>32.2 \u79fb\u52a8\u7aef\u72b6\u6001\u7801\u5904\u7406\u7b56\u7565<\/h3>\n<h4>32.2.1 \u667a\u80fd\u91cd\u8bd5\u673a\u5236<\/h4>\n<p>kotlin<\/p>\n<p>\/\/ Android\u91cd\u8bd5\u7b56\u7565\u5b9e\u73b0<br \/>\nclass SmartRetryInterceptor : Interceptor {<br \/>\n    private val maxRetries &#061; 3<br \/>\n    private val retryDelayBase &#061; 1000L \/\/ 1\u79d2<\/p>\n<p>    override fun intercept(chain: Interceptor.Chain): Response {<br \/>\n        val request &#061; chain.request()<br \/>\n        var response: Response? &#061; null<br \/>\n        var lastException: IOException? &#061; null<\/p>\n<p>        for (attempt in 1..maxRetries) {<br \/>\n            try {<br \/>\n                response &#061; chain.proceed(request)<\/p>\n<p>                \/\/ \u6839\u636e\u72b6\u6001\u7801\u51b3\u5b9a\u662f\u5426\u91cd\u8bd5<br \/>\n                when (response.code) {<br \/>\n                    in 500..599 -&gt; {<br \/>\n                        \/\/ \u670d\u52a1\u5668\u9519\u8bef&#xff0c;\u9700\u8981\u91cd\u8bd5<br \/>\n                        if (attempt &lt; maxRetries) {<br \/>\n                            response.close()<br \/>\n                            delay(calculateBackoff(attempt))<br \/>\n                            continue<br \/>\n                        }<br \/>\n                    }<br \/>\n                    408 -&gt; { \/\/ \u8bf7\u6c42\u8d85\u65f6<br \/>\n                        if (attempt &lt; maxRetries) {<br \/>\n                            response.close()<br \/>\n                            delay(calculateBackoff(attempt))<br \/>\n                            continue<br \/>\n                        }<br \/>\n                    }<br \/>\n                    429 -&gt; { \/\/ \u8bf7\u6c42\u8fc7\u591a<br \/>\n                        val retryAfter &#061; response.headers[&#034;Retry-After&#034;]?.toLongOrNull()<br \/>\n                        if (retryAfter !&#061; null) {<br \/>\n                            response.close()<br \/>\n                            delay(retryAfter * 1000)<br \/>\n                            continue<br \/>\n                        }<br \/>\n                    }<br \/>\n                    503 -&gt; { \/\/ \u670d\u52a1\u4e0d\u53ef\u7528<br \/>\n                        val retryAfter &#061; response.headers[&#034;Retry-After&#034;]?.toLongOrNull()<br \/>\n                        if (retryAfter !&#061; null &amp;&amp; attempt &lt; maxRetries) {<br \/>\n                            response.close()<br \/>\n                            delay(retryAfter * 1000)<br \/>\n                            continue<br \/>\n                        }<br \/>\n                    }<br \/>\n                }<\/p>\n<p>                return response<\/p>\n<p>            } catch (e: IOException) {<br \/>\n                lastException &#061; e<\/p>\n<p>                \/\/ \u7f51\u7edc\u5f02\u5e38&#xff0c;\u6839\u636e\u7f51\u7edc\u7c7b\u578b\u51b3\u5b9a\u662f\u5426\u91cd\u8bd5<br \/>\n                if (isNetworkErrorRecoverable(e) &amp;&amp; attempt &lt; maxRetries) {<br \/>\n                    delay(calculateBackoff(attempt))<br \/>\n                    continue<br \/>\n                }<br \/>\n                break<br \/>\n            }<br \/>\n        }<\/p>\n<p>        throw lastException ?: IOException(&#034;Request failed after $maxRetries attempts&#034;)<br \/>\n    }<\/p>\n<p>    private fun calculateBackoff(attempt: Int): Long {<br \/>\n        \/\/ \u6307\u6570\u9000\u907f\u7b97\u6cd5<br \/>\n        return retryDelayBase * (2.0.pow(attempt &#8211; 1).toLong())<br \/>\n    }<\/p>\n<p>    private fun isNetworkErrorRecoverable(e: IOException): Boolean {<br \/>\n        return when {<br \/>\n            e is SocketTimeoutException -&gt; true<br \/>\n            e is ConnectException -&gt; true<br \/>\n            e.message?.contains(&#034;ETIMEDOUT&#034;) &#061;&#061; true -&gt; true<br \/>\n            e.message?.contains(&#034;ENETUNREACH&#034;) &#061;&#061; true -&gt; false \/\/ \u7f51\u7edc\u4e0d\u53ef\u8fbe&#xff0c;\u4e0d\u91cd\u8bd5<br \/>\n            else -&gt; true<br \/>\n        }<br \/>\n    }<\/p>\n<p>    private fun delay(millis: Long) {<br \/>\n        try {<br \/>\n            Thread.sleep(millis)<br \/>\n        } catch (e: InterruptedException) {<br \/>\n            Thread.currentThread().interrupt()<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<h4>32.2.2 \u79bb\u7ebf\u4f18\u5148\u67b6\u6784<\/h4>\n<p>dart<\/p>\n<p>\/\/ Flutter\u79bb\u7ebf\u4f18\u5148\u5b9e\u73b0<br \/>\nclass OfflineFirstRepository {<br \/>\n  final LocalDataSource local;<br \/>\n  final RemoteDataSource remote;<br \/>\n  final Connectivity connectivity;<\/p>\n<p>  OfflineFirstRepository({<br \/>\n    required this.local,<br \/>\n    required this.remote,<br \/>\n    required this.connectivity,<br \/>\n  });<\/p>\n<p>  Future&lt;Result&lt;T&gt;&gt; fetchData&lt;T&gt;({<br \/>\n    required String key,<br \/>\n    required Future&lt;T&gt; Function() remoteFetch,<br \/>\n    Duration cacheDuration &#061; const Duration(hours: 1),<br \/>\n  }) async {<br \/>\n    \/\/ 1. \u9996\u5148\u5c1d\u8bd5\u4ece\u7f13\u5b58\u83b7\u53d6<br \/>\n    final cached &#061; await local.get&lt;T&gt;(key);<br \/>\n    if (cached !&#061; null &amp;&amp; !isCacheExpired(cached, cacheDuration)) {<br \/>\n      return Result.success(cached.data, source: DataSource.cache);<br \/>\n    }<\/p>\n<p>    \/\/ 2. \u68c0\u67e5\u7f51\u7edc\u8fde\u63a5<br \/>\n    final isConnected &#061; await connectivity.checkConnection();<\/p>\n<p>    if (!isConnected) {<br \/>\n      \/\/ \u79bb\u7ebf\u72b6\u6001&#xff0c;\u8fd4\u56de\u7f13\u5b58\u6570\u636e\u6216\u9519\u8bef<br \/>\n      if (cached !&#061; null) {<br \/>\n        return Result.success(<br \/>\n          cached.data,<br \/>\n          source: DataSource.cache,<br \/>\n          isStale: true,<br \/>\n        );<br \/>\n      } else {<br \/>\n        return Result.error(<br \/>\n          error: NetworkError.offline(),<br \/>\n          source: DataSource.none,<br \/>\n        );<br \/>\n      }<br \/>\n    }<\/p>\n<p>    try {<br \/>\n      \/\/ 3. \u7f51\u7edc\u53ef\u7528&#xff0c;\u4ece\u8fdc\u7a0b\u83b7\u53d6<br \/>\n      final remoteData &#061; await remoteFetch();<\/p>\n<p>      \/\/ 4. \u66f4\u65b0\u7f13\u5b58<br \/>\n      await local.save(<br \/>\n        key: key,<br \/>\n        data: remoteData,<br \/>\n        timestamp: DateTime.now(),<br \/>\n      );<\/p>\n<p>      return Result.success(remoteData, source: DataSource.remote);<\/p>\n<p>    } catch (error) {<br \/>\n      \/\/ 5. \u7f51\u7edc\u8bf7\u6c42\u5931\u8d25&#xff0c;\u56de\u9000\u5230\u7f13\u5b58<br \/>\n      if (cached !&#061; null) {<br \/>\n        return Result.success(<br \/>\n          cached.data,<br \/>\n          source: DataSource.cache,<br \/>\n          isStale: true,<br \/>\n          error: error,<br \/>\n        );<br \/>\n      } else {<br \/>\n        return Result.error(<br \/>\n          error: error,<br \/>\n          source: DataSource.none,<br \/>\n        );<br \/>\n      }<br \/>\n    }<br \/>\n  }<\/p>\n<p>  Future&lt;Result&lt;T&gt;&gt; postData&lt;T&gt;({<br \/>\n    required String endpoint,<br \/>\n    required dynamic data,<br \/>\n    required T Function(dynamic) fromJson,<br \/>\n  }) async {<br \/>\n    final isConnected &#061; await connectivity.checkConnection();<\/p>\n<p>    if (!isConnected) {<br \/>\n      \/\/ \u79bb\u7ebf\u72b6\u6001&#xff0c;\u4fdd\u5b58\u5230\u672c\u5730\u961f\u5217<br \/>\n      final operation &#061; OfflineOperation(<br \/>\n        id: Uuid().v4(),<br \/>\n        type: OperationType.post,<br \/>\n        endpoint: endpoint,<br \/>\n        data: data,<br \/>\n        createdAt: DateTime.now(),<br \/>\n        status: OfflineStatus.pending,<br \/>\n      );<\/p>\n<p>      await local.saveOfflineOperation(operation);<\/p>\n<p>      return Result.success(<br \/>\n        fromJson(data), \/\/ \u4e50\u89c2\u66f4\u65b0<br \/>\n        source: DataSource.local,<br \/>\n        isOffline: true,<br \/>\n        offlineOperationId: operation.id,<br \/>\n      );<br \/>\n    }<\/p>\n<p>    try {<br \/>\n      \/\/ \u5728\u7ebf\u72b6\u6001&#xff0c;\u76f4\u63a5\u53d1\u9001<br \/>\n      final response &#061; await remote.post(endpoint, data);<\/p>\n<p>      if (response.statusCode &#061;&#061; 201 || response.statusCode &#061;&#061; 200) {<br \/>\n        final result &#061; fromJson(response.data);<br \/>\n        return Result.success(result, source: DataSource.remote);<br \/>\n      } else {<br \/>\n        throw HttpException(response.statusCode, response.data);<br \/>\n      }<\/p>\n<p>    } catch (error) {<br \/>\n      \/\/ \u7f51\u7edc\u9519\u8bef&#xff0c;\u4fdd\u5b58\u5230\u79bb\u7ebf\u961f\u5217<br \/>\n      if (error is SocketException || error is TimeoutException) {<br \/>\n        final operation &#061; OfflineOperation(<br \/>\n          id: Uuid().v4(),<br \/>\n          type: OperationType.post,<br \/>\n          endpoint: endpoint,<br \/>\n          data: data,<br \/>\n          createdAt: DateTime.now(),<br \/>\n          status: OfflineStatus.failed,<br \/>\n          lastError: error.toString(),<br \/>\n        );<\/p>\n<p>        await local.saveOfflineOperation(operation);<\/p>\n<p>        return Result.success(<br \/>\n          fromJson(data), \/\/ \u4e50\u89c2\u66f4\u65b0<br \/>\n          source: DataSource.local,<br \/>\n          isOffline: true,<br \/>\n          offlineOperationId: operation.id,<br \/>\n          error: error,<br \/>\n        );<br \/>\n      }<\/p>\n<p>      rethrow;<br \/>\n    }<br \/>\n  }<\/p>\n<p>  Future&lt;void&gt; syncOfflineOperations() async {<br \/>\n    final operations &#061; await local.getPendingOperations();<\/p>\n<p>    for (final operation in operations) {<br \/>\n      try {<br \/>\n        dynamic response;<\/p>\n<p>        switch (operation.type) {<br \/>\n          case OperationType.post:<br \/>\n            response &#061; await remote.post(operation.endpoint, operation.data);<br \/>\n            break;<br \/>\n          case OperationType.put:<br \/>\n            response &#061; await remote.put(operation.endpoint, operation.data);<br \/>\n            break;<br \/>\n          case OperationType.delete:<br \/>\n            response &#061; await remote.delete(operation.endpoint);<br \/>\n            break;<br \/>\n        }<\/p>\n<p>        \/\/ \u540c\u6b65\u6210\u529f&#xff0c;\u6807\u8bb0\u4e3a\u5df2\u5b8c\u6210<br \/>\n        await local.updateOperationStatus(<br \/>\n          operation.id,<br \/>\n          OfflineStatus.completed,<br \/>\n        );<\/p>\n<p>      } catch (error) {<br \/>\n        \/\/ \u540c\u6b65\u5931\u8d25&#xff0c;\u66f4\u65b0\u9519\u8bef\u4fe1\u606f<br \/>\n        await local.updateOperationError(<br \/>\n          operation.id,<br \/>\n          error.toString(),<br \/>\n        );<\/p>\n<p>        \/\/ \u6839\u636e\u9519\u8bef\u7c7b\u578b\u51b3\u5b9a\u662f\u5426\u7ee7\u7eed\u91cd\u8bd5<br \/>\n        if (error is HttpException &amp;&amp; error.statusCode &gt;&#061; 500) {<br \/>\n          \/\/ \u670d\u52a1\u5668\u9519\u8bef&#xff0c;\u7a0d\u540e\u91cd\u8bd5<br \/>\n          continue;<br \/>\n        } else if (error is SocketException) {<br \/>\n          \/\/ \u7f51\u7edc\u9519\u8bef&#xff0c;\u505c\u6b62\u540c\u6b65<br \/>\n          break;<br \/>\n        }<br \/>\n      }<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<h3>32.3 \u79fb\u52a8\u7aef\u72b6\u6001\u7801\u7684UI\u53cd\u9988<\/h3>\n<h4>32.3.1 \u4f18\u96c5\u7684\u9519\u8bef\u63d0\u793a<\/h4>\n<p>swift<\/p>\n<p>\/\/ iOS\u9519\u8bef\u63d0\u793a\u7ec4\u4ef6<br \/>\nclass ErrorToastView: UIView {<br \/>\n    private let titleLabel &#061; UILabel()<br \/>\n    private let messageLabel &#061; UILabel()<br \/>\n    private let iconImageView &#061; UIImageView()<br \/>\n    private let actionButton &#061; UIButton()<br \/>\n    private var actionHandler: (() -&gt; Void)?<\/p>\n<p>    enum ErrorType {<br \/>\n        case networkError<br \/>\n        case serverError<br \/>\n        case clientError(Int)<br \/>\n        case offline<br \/>\n        case timeout<br \/>\n    }<\/p>\n<p>    static func show(<br \/>\n        for error: Error,<br \/>\n        in viewController: UIViewController,<br \/>\n        autoDismiss: Bool &#061; true<br \/>\n    ) {<br \/>\n        let errorView &#061; ErrorToastView()<br \/>\n        errorView.configure(with: error)<\/p>\n<p>        \/\/ \u6dfb\u52a0\u5230\u89c6\u56fe<br \/>\n        viewController.view.addSubview(errorView)<\/p>\n<p>        \/\/ \u52a8\u753b\u663e\u793a<br \/>\n        UIView.animate(withDuration: 0.3) {<br \/>\n            errorView.alpha &#061; 1<br \/>\n            errorView.transform &#061; .identity<br \/>\n        }<\/p>\n<p>        if autoDismiss {<br \/>\n            DispatchQueue.main.asyncAfter(deadline: .now() &#043; 5) {<br \/>\n                errorView.dismiss()<br \/>\n            }<br \/>\n        }<br \/>\n    }<\/p>\n<p>    private func configure(with error: Error) {<br \/>\n        var title &#061; &#034;\u9519\u8bef&#034;<br \/>\n        var message &#061; error.localizedDescription<br \/>\n        var iconName &#061; &#034;exclamationmark.circle&#034;<br \/>\n        var actionTitle: String? &#061; nil<\/p>\n<p>        if let httpError &#061; error as? HTTPError {<br \/>\n            switch httpError.statusCode {<br \/>\n            case 400:<br \/>\n                title &#061; &#034;\u8bf7\u6c42\u9519\u8bef&#034;<br \/>\n                message &#061; &#034;\u8bf7\u68c0\u67e5\u8f93\u5165\u5185\u5bb9&#034;<br \/>\n                iconName &#061; &#034;pencil.circle&#034;<\/p>\n<p>            case 401:<br \/>\n                title &#061; &#034;\u767b\u5f55\u8fc7\u671f&#034;<br \/>\n                message &#061; &#034;\u8bf7\u91cd\u65b0\u767b\u5f55&#034;<br \/>\n                iconName &#061; &#034;person.crop.circle.badge.exclamationmark&#034;<br \/>\n                actionTitle &#061; &#034;\u91cd\u65b0\u767b\u5f55&#034;<br \/>\n                actionHandler &#061; { [weak self] in<br \/>\n                    self?.navigateToLogin()<br \/>\n                }<\/p>\n<p>            case 403:<br \/>\n                title &#061; &#034;\u6743\u9650\u4e0d\u8db3&#034;<br \/>\n                message &#061; &#034;\u60a8\u6ca1\u6709\u6743\u9650\u6267\u884c\u6b64\u64cd\u4f5c&#034;<br \/>\n                iconName &#061; &#034;lock.circle&#034;<\/p>\n<p>            case 404:<br \/>\n                title &#061; &#034;\u5185\u5bb9\u4e0d\u5b58\u5728&#034;<br \/>\n                message &#061; &#034;\u60a8\u8bbf\u95ee\u7684\u5185\u5bb9\u53ef\u80fd\u5df2\u88ab\u5220\u9664&#034;<br \/>\n                iconName &#061; &#034;questionmark.circle&#034;<\/p>\n<p>            case 408:<br \/>\n                title &#061; &#034;\u8bf7\u6c42\u8d85\u65f6&#034;<br \/>\n                message &#061; &#034;\u7f51\u7edc\u8fde\u63a5\u8f83\u6162&#xff0c;\u8bf7\u7a0d\u540e\u91cd\u8bd5&#034;<br \/>\n                iconName &#061; &#034;clock.badge.exclamationmark&#034;<br \/>\n                actionTitle &#061; &#034;\u91cd\u8bd5&#034;<br \/>\n                actionHandler &#061; { [weak self] in<br \/>\n                    self?.retryLastRequest()<br \/>\n                }<\/p>\n<p>            case 429:<br \/>\n                title &#061; &#034;\u8bf7\u6c42\u8fc7\u4e8e\u9891\u7e41&#034;<br \/>\n                message &#061; &#034;\u8bf7\u7a0d\u540e\u518d\u8bd5&#034;<br \/>\n                iconName &#061; &#034;hand.raised.circle&#034;<\/p>\n<p>            case 500..&lt;600:<br \/>\n                title &#061; &#034;\u670d\u52a1\u5668\u9519\u8bef&#034;<br \/>\n                message &#061; &#034;\u670d\u52a1\u5668\u6682\u65f6\u4e0d\u53ef\u7528&#xff0c;\u8bf7\u7a0d\u540e\u91cd\u8bd5&#034;<br \/>\n                iconName &#061; &#034;server.rack&#034;<br \/>\n                actionTitle &#061; &#034;\u91cd\u8bd5&#034;<br \/>\n                actionHandler &#061; { [weak self] in<br \/>\n                    self?.retryLastRequest()<br \/>\n                }<\/p>\n<p>            default:<br \/>\n                break<br \/>\n            }<\/p>\n<p>        } else if let networkError &#061; error as? NetworkError {<br \/>\n            switch networkError {<br \/>\n            case .noConnection:<br \/>\n                title &#061; &#034;\u7f51\u7edc\u8fde\u63a5\u5931\u8d25&#034;<br \/>\n                message &#061; &#034;\u8bf7\u68c0\u67e5\u7f51\u7edc\u8bbe\u7f6e&#034;<br \/>\n                iconName &#061; &#034;wifi.exclamationmark&#034;<br \/>\n                actionTitle &#061; &#034;\u8bbe\u7f6e&#034;<br \/>\n                actionHandler &#061; { [weak self] in<br \/>\n                    self?.openNetworkSettings()<br \/>\n                }<\/p>\n<p>            case .timeout:<br \/>\n                title &#061; &#034;\u8fde\u63a5\u8d85\u65f6&#034;<br \/>\n                message &#061; &#034;\u7f51\u7edc\u54cd\u5e94\u7f13\u6162&#xff0c;\u8bf7\u68c0\u67e5\u7f51\u7edc&#034;<br \/>\n                iconName &#061; &#034;clock.badge.exclamationmark&#034;<\/p>\n<p>            case .cellularDataRestricted:<br \/>\n                title &#061; &#034;\u8702\u7a9d\u6570\u636e\u9650\u5236&#034;<br \/>\n                message &#061; &#034;\u5f53\u524d\u8bbe\u7f6e\u9650\u5236\u4e86\u8702\u7a9d\u6570\u636e\u4f7f\u7528&#034;<br \/>\n                iconName &#061; &#034;cellularbars&#034;<br \/>\n                actionTitle &#061; &#034;\u8bbe\u7f6e&#034;<br \/>\n                actionHandler &#061; { [weak self] in<br \/>\n                    self?.openAppSettings()<br \/>\n                }<br \/>\n            }<br \/>\n        }<\/p>\n<p>        \/\/ \u914d\u7f6eUI<br \/>\n        titleLabel.text &#061; title<br \/>\n        messageLabel.text &#061; message<br \/>\n        iconImageView.image &#061; UIImage(systemName: iconName)<\/p>\n<p>        if let actionTitle &#061; actionTitle {<br \/>\n            actionButton.setTitle(actionTitle, for: .normal)<br \/>\n            actionButton.isHidden &#061; false<br \/>\n        } else {<br \/>\n            actionButton.isHidden &#061; true<br \/>\n        }<br \/>\n    }<\/p>\n<p>    private func navigateToLogin() {<br \/>\n        \/\/ \u5bfc\u822a\u5230\u767b\u5f55\u9875\u9762<br \/>\n        dismiss()<br \/>\n    }<\/p>\n<p>    private func retryLastRequest() {<br \/>\n        \/\/ \u91cd\u8bd5\u6700\u540e\u4e00\u6b21\u8bf7\u6c42<br \/>\n        dismiss()<br \/>\n    }<\/p>\n<p>    private func openNetworkSettings() {<br \/>\n        if let url &#061; URL(string: &#034;App-Prefs:root&#061;WIFI&#034;) {<br \/>\n            UIApplication.shared.open(url)<br \/>\n        }<br \/>\n        dismiss()<br \/>\n    }<\/p>\n<p>    private func dismiss() {<br \/>\n        UIView.animate(withDuration: 0.3, animations: {<br \/>\n            self.alpha &#061; 0<br \/>\n            self.transform &#061; CGAffineTransform(translationX: 0, y: -20)<br \/>\n        }) { _ in<br \/>\n            self.removeFromSuperview()<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<h4>32.3.2 \u72b6\u6001\u7801\u9a71\u52a8\u7684UI\u72b6\u6001<\/h4>\n<p>kotlin<\/p>\n<p>\/\/ Android Compose\u72b6\u6001\u7ba1\u7406<br \/>\n&#064;Composable<br \/>\nfun DataScreen(<br \/>\n    viewModel: DataViewModel &#061; viewModel()<br \/>\n) {<br \/>\n    val uiState by viewModel.uiState.collectAsState()<\/p>\n<p>    Box(modifier &#061; Modifier.fillMaxSize()) {<br \/>\n        when (val state &#061; uiState) {<br \/>\n            is DataUiState.Loading -&gt; {<br \/>\n                LoadingView()<br \/>\n            }<\/p>\n<p>            is DataUiState.Success -&gt; {<br \/>\n                DataListView(data &#061; state.data)<br \/>\n            }<\/p>\n<p>            is DataUiState.Error -&gt; {<br \/>\n                when (state.error) {<br \/>\n                    is HttpException -&gt; {<br \/>\n                        when (state.error.code()) {<br \/>\n                            401 -&gt; {<br \/>\n                                AuthErrorView(<br \/>\n                                    message &#061; &#034;\u767b\u5f55\u5df2\u8fc7\u671f&#034;,<br \/>\n                                    onRetry &#061; { viewModel.retry() },<br \/>\n                                    onLogin &#061; { viewModel.navigateToLogin() }<br \/>\n                                )<br \/>\n                            }<br \/>\n                            403 -&gt; {<br \/>\n                                PermissionErrorView(<br \/>\n                                    message &#061; &#034;\u6743\u9650\u4e0d\u8db3&#034;,<br \/>\n                                    onRequestPermission &#061; { viewModel.requestPermission() }<br \/>\n                                )<br \/>\n                            }<br \/>\n                            404 -&gt; {<br \/>\n                                NotFoundView(<br \/>\n                                    message &#061; &#034;\u5185\u5bb9\u4e0d\u5b58\u5728&#034;,<br \/>\n                                    onGoBack &#061; { viewModel.navigateBack() }<br \/>\n                                )<br \/>\n                            }<br \/>\n                            408, 504 -&gt; {<br \/>\n                                TimeoutErrorView(<br \/>\n                                    message &#061; &#034;\u8bf7\u6c42\u8d85\u65f6&#034;,<br \/>\n                                    onRetry &#061; { viewModel.retry() }<br \/>\n                                )<br \/>\n                            }<br \/>\n                            429 -&gt; {<br \/>\n                                RateLimitView(<br \/>\n                                    message &#061; &#034;\u8bf7\u6c42\u8fc7\u4e8e\u9891\u7e41&#034;,<br \/>\n                                    retryAfter &#061; state.error.getRetryAfter(),<br \/>\n                                    onRetry &#061; { viewModel.retry() }<br \/>\n                                )<br \/>\n                            }<br \/>\n                            in 500..599 -&gt; {<br \/>\n                                ServerErrorView(<br \/>\n                                    message &#061; &#034;\u670d\u52a1\u5668\u9519\u8bef&#034;,<br \/>\n                                    onRetry &#061; { viewModel.retry() },<br \/>\n                                    onRefresh &#061; { viewModel.refresh() }<br \/>\n                                )<br \/>\n                            }<br \/>\n                            else -&gt; {<br \/>\n                                GenericErrorView(<br \/>\n                                    error &#061; state.error,<br \/>\n                                    onRetry &#061; { viewModel.retry() }<br \/>\n                                )<br \/>\n                            }<br \/>\n                        }<br \/>\n                    }<\/p>\n<p>                    is IOException -&gt; {<br \/>\n                        NetworkErrorView(<br \/>\n                            message &#061; &#034;\u7f51\u7edc\u8fde\u63a5\u5931\u8d25&#034;,<br \/>\n                            onRetry &#061; { viewModel.retry() },<br \/>\n                            onCheckNetwork &#061; { viewModel.openNetworkSettings() }<br \/>\n                        )<br \/>\n                    }<\/p>\n<p>                    else -&gt; {<br \/>\n                        GenericErrorView(<br \/>\n                            error &#061; state.error,<br \/>\n                            onRetry &#061; { viewModel.retry() }<br \/>\n                        )<br \/>\n                    }<br \/>\n                }<br \/>\n            }<\/p>\n<p>            is DataUiState.Offline -&gt; {<br \/>\n                OfflineView(<br \/>\n                    data &#061; state.cachedData,<br \/>\n                    isStale &#061; state.isStale,<br \/>\n                    onRefresh &#061; { viewModel.refresh() }<br \/>\n                )<br \/>\n            }<br \/>\n        }<\/p>\n<p>        \/\/ \u5168\u5c40\u7f51\u7edc\u72b6\u6001\u6307\u793a\u5668<br \/>\n        NetworkStatusIndicator(<br \/>\n            isOnline &#061; viewModel.isOnline,<br \/>\n            isCellular &#061; viewModel.isCellular<br \/>\n        )<br \/>\n    }<br \/>\n}<\/p>\n<p>\/\/ \u7f51\u7edc\u72b6\u6001\u6307\u793a\u5668\u7ec4\u4ef6<br \/>\n&#064;Composable<br \/>\nfun NetworkStatusIndicator(<br \/>\n    isOnline: Boolean,<br \/>\n    isCellular: Boolean<br \/>\n) {<br \/>\n    AnimatedVisibility(<br \/>\n        visible &#061; !isOnline,<br \/>\n        enter &#061; slideInVertically(initialOffsetY &#061; { -it }),<br \/>\n        exit &#061; slideOutVertically(targetOffsetY &#061; { -it })<br \/>\n    ) {<br \/>\n        Surface(<br \/>\n            modifier &#061; Modifier.fillMaxWidth(),<br \/>\n            color &#061; MaterialTheme.colors.error.copy(alpha &#061; 0.9f)<br \/>\n        ) {<br \/>\n            Row(<br \/>\n                modifier &#061; Modifier<br \/>\n                    .fillMaxWidth()<br \/>\n                    .padding(12.dp),<br \/>\n                verticalAlignment &#061; Alignment.CenterVertically<br \/>\n            ) {<br \/>\n                Icon(<br \/>\n                    imageVector &#061; Icons.Default.CloudOff,<br \/>\n                    contentDescription &#061; &#034;\u79bb\u7ebf&#034;,<br \/>\n                    tint &#061; Color.White<br \/>\n                )<\/p>\n<p>                Spacer(modifier &#061; Modifier.width(8.dp))<\/p>\n<p>                Text(<br \/>\n                    text &#061; if (isCellular) {<br \/>\n                        &#034;\u5f53\u524d\u4f7f\u7528\u8702\u7a9d\u6570\u636e&#xff0c;\u5efa\u8bae\u8fde\u63a5Wi-Fi&#034;<br \/>\n                    } else {<br \/>\n                        &#034;\u7f51\u7edc\u8fde\u63a5\u5df2\u65ad\u5f00&#034;<br \/>\n                    },<br \/>\n                    color &#061; Color.White,<br \/>\n                    fontSize &#061; 14.sp<br \/>\n                )<br \/>\n            }<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<h3>32.4 \u79fb\u52a8\u7aef\u6027\u80fd\u4f18\u5316<\/h3>\n<h4>32.4.1 \u72b6\u6001\u7801\u9a71\u52a8\u7684\u7f13\u5b58\u7b56\u7565<\/h4>\n<p>typescript<\/p>\n<p>\/\/ React Native\u7f13\u5b58\u7ba1\u7406\u5668<br \/>\nclass CacheManager {<br \/>\n  private cache: AsyncStorage;<br \/>\n  private maxAge: number;<br \/>\n  private networkFirstFor: number[]; \/\/ \u8fd9\u4e9b\u72b6\u6001\u7801\u4f18\u5148\u4f7f\u7528\u7f51\u7edc<\/p>\n<p>  constructor() {<br \/>\n    this.cache &#061; AsyncStorage;<br \/>\n    this.maxAge &#061; 5 * 60 * 1000; \/\/ 5\u5206\u949f<br \/>\n    this.networkFirstFor &#061; [401, 403, 404, 422];<br \/>\n  }<\/p>\n<p>  async get&lt;T&gt;(<br \/>\n    key: string,<br \/>\n    fetcher: () &#061;&gt; Promise&lt;T&gt;,<br \/>\n    options: CacheOptions &#061; {}<br \/>\n  ): Promise&lt;CacheResult&lt;T&gt;&gt; {<br \/>\n    const {<br \/>\n      forceNetwork &#061; false,<br \/>\n      maxAge &#061; this.maxAge,<br \/>\n      staleWhileRevalidate &#061; true<br \/>\n    } &#061; options;<\/p>\n<p>    \/\/ 1. \u68c0\u67e5\u7f13\u5b58<br \/>\n    const cached &#061; await this.getFromCache&lt;T&gt;(key);<br \/>\n    const isCacheValid &#061; cached &amp;&amp; !this.isExpired(cached.timestamp, maxAge);<\/p>\n<p>    \/\/ 2. \u5982\u679c\u5f3a\u5236\u4f7f\u7528\u7f51\u7edc\u6216\u7f13\u5b58\u65e0\u6548&#xff0c;\u76f4\u63a5\u83b7\u53d6\u7f51\u7edc\u6570\u636e<br \/>\n    if (forceNetwork || !isCacheValid) {<br \/>\n      return this.fetchAndCache(key, fetcher);<br \/>\n    }<\/p>\n<p>    \/\/ 3. \u5982\u679c\u7f13\u5b58\u6709\u6548&#xff0c;\u7acb\u5373\u8fd4\u56de\u7f13\u5b58\u6570\u636e<br \/>\n    const result: CacheResult&lt;T&gt; &#061; {<br \/>\n      data: cached.data,<br \/>\n      source: &#039;cache&#039;,<br \/>\n      timestamp: cached.timestamp<br \/>\n    };<\/p>\n<p>    \/\/ 4. \u5982\u679c\u9700\u8981&#xff0c;\u5728\u540e\u53f0\u66f4\u65b0\u7f13\u5b58<br \/>\n    if (staleWhileRevalidate &amp;&amp; !this.isExpired(cached.timestamp, maxAge * 2)) {<br \/>\n      this.backgroundRefresh(key, fetcher);<br \/>\n    }<\/p>\n<p>    return result;<br \/>\n  }<\/p>\n<p>  private async fetchAndCache&lt;T&gt;(<br \/>\n    key: string,<br \/>\n    fetcher: () &#061;&gt; Promise&lt;T&gt;<br \/>\n  ): Promise&lt;CacheResult&lt;T&gt;&gt; {<br \/>\n    try {<br \/>\n      const data &#061; await fetcher();<br \/>\n      const timestamp &#061; Date.now();<\/p>\n<p>      \/\/ \u4fdd\u5b58\u5230\u7f13\u5b58<br \/>\n      await this.cache.setItem(<br \/>\n        key,<br \/>\n        JSON.stringify({ data, timestamp })<br \/>\n      );<\/p>\n<p>      return {<br \/>\n        data,<br \/>\n        source: &#039;network&#039;,<br \/>\n        timestamp<br \/>\n      };<\/p>\n<p>    } catch (error) {<br \/>\n      \/\/ \u7f51\u7edc\u5931\u8d25&#xff0c;\u5c1d\u8bd5\u8fd4\u56de\u7f13\u5b58<br \/>\n      const cached &#061; await this.getFromCache&lt;T&gt;(key);<\/p>\n<p>      if (cached) {<br \/>\n        return {<br \/>\n          data: cached.data,<br \/>\n          source: &#039;cache&#039;,<br \/>\n          timestamp: cached.timestamp,<br \/>\n          isStale: true,<br \/>\n          error<br \/>\n        };<br \/>\n      }<\/p>\n<p>      throw error;<br \/>\n    }<br \/>\n  }<\/p>\n<p>  private async backgroundRefresh(key: string, fetcher: () &#061;&gt; Promise&lt;T&gt;) {<br \/>\n    \/\/ \u4f7f\u7528\u4f4e\u4f18\u5148\u7ea7\u8fdb\u884c\u540e\u53f0\u5237\u65b0<br \/>\n    InteractionManager.runAfterInteractions(async () &#061;&gt; {<br \/>\n      try {<br \/>\n        const data &#061; await fetcher();<br \/>\n        const timestamp &#061; Date.now();<\/p>\n<p>        await this.cache.setItem(<br \/>\n          key,<br \/>\n          JSON.stringify({ data, timestamp })<br \/>\n        );<\/p>\n<p>        \/\/ \u901a\u77e5\u6570\u636e\u5df2\u66f4\u65b0<br \/>\n        DeviceEventEmitter.emit(&#039;cache_updated&#039;, { key });<\/p>\n<p>      } catch (error) {<br \/>\n        console.warn(&#096;Background refresh failed for ${key}:&#096;, error);<br \/>\n      }<br \/>\n    });<br \/>\n  }<\/p>\n<p>  \/\/ \u57fa\u4e8e\u72b6\u6001\u7801\u7684\u7f13\u5b58\u6e05\u7406\u7b56\u7565<br \/>\n  async handleResponseError(<br \/>\n    key: string,<br \/>\n    statusCode: number,<br \/>\n    error: any<br \/>\n  ): Promise&lt;void&gt; {<br \/>\n    switch (statusCode) {<br \/>\n      case 401: \/\/ \u8ba4\u8bc1\u5931\u8d25<br \/>\n      case 403: \/\/ \u6743\u9650\u53d8\u66f4<br \/>\n        \/\/ \u6e05\u9664\u7528\u6237\u76f8\u5173\u7f13\u5b58<br \/>\n        await this.clearUserRelatedCache();<br \/>\n        break;<\/p>\n<p>      case 404: \/\/ \u8d44\u6e90\u4e0d\u5b58\u5728<br \/>\n        \/\/ \u6e05\u9664\u8be5\u8d44\u6e90\u7684\u7f13\u5b58<br \/>\n        await this.cache.removeItem(key);<br \/>\n        break;<\/p>\n<p>      case 410: \/\/ \u8d44\u6e90\u5df2\u5220\u9664<br \/>\n        \/\/ \u6e05\u9664\u7f13\u5b58\u5e76\u6807\u8bb0\u4e3a\u5df2\u5220\u9664<br \/>\n        await this.cache.setItem(<br \/>\n          &#096;${key}_deleted&#096;,<br \/>\n          JSON.stringify({ deletedAt: Date.now() })<br \/>\n        );<br \/>\n        await this.cache.removeItem(key);<br \/>\n        break;<\/p>\n<p>      case 422: \/\/ \u9a8c\u8bc1\u9519\u8bef<br \/>\n        \/\/ \u4fdd\u7559\u7f13\u5b58&#xff0c;\u4f46\u6807\u8bb0\u4e3a\u53ef\u80fd\u8fc7\u65f6<br \/>\n        await this.cache.setItem(<br \/>\n          &#096;${key}_stale&#096;,<br \/>\n          JSON.stringify({ lastError: error, timestamp: Date.now() })<br \/>\n        );<br \/>\n        break;<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<h4>32.4.2 \u56fe\u7247\u52a0\u8f7d\u4f18\u5316<\/h4>\n<p>swift<\/p>\n<p>\/\/ iOS\u56fe\u7247\u52a0\u8f7d\u5668<br \/>\nclass SmartImageLoader {<br \/>\n    private let memoryCache &#061; NSCache&lt;NSString, UIImage&gt;()<br \/>\n    private let diskCache &#061; URLCache.shared<br \/>\n    private let session: URLSession<br \/>\n    private let maxConcurrentDownloads &#061; 3<\/p>\n<p>    \/\/ \u7f51\u7edc\u72b6\u51b5\u611f\u77e5\u7684\u56fe\u7247\u8d28\u91cf<br \/>\n    func optimalImageURL(for originalURL: URL, networkType: NetworkType) -&gt; URL {<br \/>\n        var components &#061; URLComponents(url: originalURL, resolvingAgainstBaseURL: false)<\/p>\n<p>        switch networkType {<br \/>\n        case .wifi:<br \/>\n            \/\/ Wi-Fi\u4e0b\u4f7f\u7528\u9ad8\u8d28\u91cf\u56fe\u7247<br \/>\n            components?.queryItems &#061; [<br \/>\n                URLQueryItem(name: &#034;quality&#034;, value: &#034;high&#034;),<br \/>\n                URLQueryItem(name: &#034;width&#034;, value: &#034;1080&#034;)<br \/>\n            ]<\/p>\n<p>        case .cellular:<br \/>\n            \/\/ \u8702\u7a9d\u7f51\u7edc\u4e0b\u4f7f\u7528\u4e2d\u7b49\u8d28\u91cf<br \/>\n            components?.queryItems &#061; [<br \/>\n                URLQueryItem(name: &#034;quality&#034;, value: &#034;medium&#034;),<br \/>\n                URLQueryItem(name: &#034;width&#034;, value: &#034;720&#034;)<br \/>\n            ]<\/p>\n<p>        case .slow:<br \/>\n            \/\/ \u6162\u901f\u7f51\u7edc\u4f7f\u7528\u4f4e\u8d28\u91cf<br \/>\n            components?.queryItems &#061; [<br \/>\n                URLQueryItem(name: &#034;quality&#034;, value: &#034;low&#034;),<br \/>\n                URLQueryItem(name: &#034;width&#034;, value: &#034;480&#034;)<br \/>\n            ]<\/p>\n<p>        case .disconnected:<br \/>\n            \/\/ \u79bb\u7ebf\u72b6\u6001&#xff0c;\u8fd4\u56de\u5360\u4f4d\u56fe<br \/>\n            return Bundle.main.url(forResource: &#034;placeholder&#034;, withExtension: &#034;png&#034;)!<br \/>\n        }<\/p>\n<p>        return components?.url ?? originalURL<br \/>\n    }<\/p>\n<p>    func loadImage(<br \/>\n        from url: URL,<br \/>\n        into imageView: UIImageView,<br \/>\n        placeholder: UIImage? &#061; nil<br \/>\n    ) {<br \/>\n        let cacheKey &#061; url.absoluteString as NSString<\/p>\n<p>        \/\/ 1. \u68c0\u67e5\u5185\u5b58\u7f13\u5b58<br \/>\n        if let cachedImage &#061; memoryCache.object(forKey: cacheKey) {<br \/>\n            imageView.image &#061; cachedImage<br \/>\n            return<br \/>\n        }<\/p>\n<p>        \/\/ 2. \u8bbe\u7f6e\u5360\u4f4d\u56fe<br \/>\n        imageView.image &#061; placeholder<\/p>\n<p>        \/\/ 3. \u68c0\u67e5\u7f51\u7edc\u72b6\u51b5<br \/>\n        let networkType &#061; NetworkMonitor.shared.currentType<br \/>\n        let optimalURL &#061; optimalImageURL(for: url, networkType: networkType)<\/p>\n<p>        \/\/ 4. \u5982\u679c\u662f\u79bb\u7ebf\u72b6\u6001\u4e14\u6ca1\u6709\u7f13\u5b58&#xff0c;\u76f4\u63a5\u8fd4\u56de<br \/>\n        if networkType &#061;&#061; .disconnected {<br \/>\n            \/\/ \u5c1d\u8bd5\u4ece\u78c1\u76d8\u52a0\u8f7d\u4e0a\u6b21\u7f13\u5b58\u7684\u56fe\u7247<br \/>\n            if let diskCached &#061; loadFromDiskCache(url: url) {<br \/>\n                imageView.image &#061; diskCached<br \/>\n            }<br \/>\n            return<br \/>\n        }<\/p>\n<p>        \/\/ 5. \u5f02\u6b65\u52a0\u8f7d\u56fe\u7247<br \/>\n        DispatchQueue.global(qos: .userInitiated).async {<br \/>\n            let task &#061; self.session.dataTask(with: optimalURL) { [weak self] data, response, error in<br \/>\n                guard let self &#061; self else { return }<\/p>\n<p>                DispatchQueue.main.async {<br \/>\n                    if let error &#061; error {<br \/>\n                        self.handleImageLoadError(<br \/>\n                            error: error,<br \/>\n                            url: url,<br \/>\n                            imageView: imageView,<br \/>\n                            placeholder: placeholder<br \/>\n                        )<br \/>\n                        return<br \/>\n                    }<\/p>\n<p>                    guard let data &#061; data,<br \/>\n                          let image &#061; UIImage(data: data),<br \/>\n                          let response &#061; response else {<br \/>\n                        return<br \/>\n                    }<\/p>\n<p>                    \/\/ \u6839\u636e\u72b6\u6001\u7801\u5904\u7406<br \/>\n                    if let httpResponse &#061; response as? HTTPURLResponse {<br \/>\n                        switch httpResponse.statusCode {<br \/>\n                        case 200:<br \/>\n                            \/\/ \u6210\u529f&#xff0c;\u66f4\u65b0UI\u5e76\u7f13\u5b58<br \/>\n                            imageView.image &#061; image<br \/>\n                            self.cacheImage(image, for: cacheKey)<\/p>\n<p>                        case 304:<br \/>\n                            \/\/ \u672a\u4fee\u6539&#xff0c;\u4f7f\u7528\u7f13\u5b58<br \/>\n                            if let cached &#061; self.memoryCache.object(forKey: cacheKey) {<br \/>\n                                imageView.image &#061; cached<br \/>\n                            }<\/p>\n<p>                        case 404, 410:<br \/>\n                            \/\/ \u56fe\u7247\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664<br \/>\n                            imageView.image &#061; UIImage(named: &#034;image_not_found&#034;)<\/p>\n<p>                        default:<br \/>\n                            \/\/ \u5176\u4ed6\u9519\u8bef<br \/>\n                            imageView.image &#061; placeholder<br \/>\n                        }<br \/>\n                    }<br \/>\n                }<br \/>\n            }<\/p>\n<p>            task.resume()<br \/>\n        }<br \/>\n    }<\/p>\n<p>    private func handleImageLoadError(<br \/>\n        error: Error,<br \/>\n        url: URL,<br \/>\n        imageView: UIImageView,<br \/>\n        placeholder: UIImage?<br \/>\n    ) {<br \/>\n        if let urlError &#061; error as? URLError {<br \/>\n            switch urlError.code {<br \/>\n            case .timedOut:<br \/>\n                \/\/ \u8d85\u65f6&#xff0c;\u5c1d\u8bd5\u4f7f\u7528\u4f4e\u8d28\u91cf\u7248\u672c<br \/>\n                if let lowQualityURL &#061; getLowQualityVersion(of: url) {<br \/>\n                    loadImage(from: lowQualityURL, into: imageView, placeholder: placeholder)<br \/>\n                }<\/p>\n<p>            case .notConnectedToInternet:<br \/>\n                \/\/ \u65e0\u7f51\u7edc&#xff0c;\u5c1d\u8bd5\u4ece\u78c1\u76d8\u52a0\u8f7d<br \/>\n                if let diskCached &#061; loadFromDiskCache(url: url) {<br \/>\n                    imageView.image &#061; diskCached<br \/>\n                } else {<br \/>\n                    imageView.image &#061; placeholder<br \/>\n                }<\/p>\n<p>            case .cancelled:<br \/>\n                \/\/ \u8bf7\u6c42\u88ab\u53d6\u6d88&#xff0c;\u5ffd\u7565<br \/>\n                break<\/p>\n<p>            default:<br \/>\n                imageView.image &#061; placeholder<br \/>\n            }<br \/>\n        } else {<br \/>\n            imageView.image &#061; placeholder<br \/>\n        }<br \/>\n    }<br \/>\n}<\/p>\n<h3>32.5 \u79fb\u52a8\u7aef\u5b89\u5168\u8003\u8651<\/h3>\n<h4>32.5.1 \u5b89\u5168\u7684\u9519\u8bef\u5904\u7406<\/h4>\n<p>java<\/p>\n<p>\/\/ Android\u5b89\u5168\u9519\u8bef\u5904\u7406<br \/>\npublic class SecureErrorHandler {<br \/>\n    private static final String TAG &#061; &#034;SecureErrorHandler&#034;;<\/p>\n<p>    public static void handleException(Context context, Exception exception) {<br \/>\n        \/\/ \u8bb0\u5f55\u9519\u8bef\u4f46\u4e0d\u6cc4\u9732\u654f\u611f\u4fe1\u606f<br \/>\n        if (exception instanceof HttpException) {<br \/>\n            HttpException httpException &#061; (HttpException) exception;<\/p>\n<p>            logSafeError(&#034;HTTP Error&#034;, httpException.code());<\/p>\n<p>            \/\/ \u6839\u636e\u72b6\u6001\u7801\u663e\u793a\u7528\u6237\u53cb\u597d\u7684\u6d88\u606f<br \/>\n            String userMessage &#061; getSafeUserMessage(context, httpException.code());<\/p>\n<p>            \/\/ \u663e\u793a\u7ed9\u7528\u6237<br \/>\n            showUserFriendlyError(context, userMessage);<\/p>\n<p>        } else if (exception instanceof SSLException) {<br \/>\n            \/\/ SSL\u9519\u8bef&#xff0c;\u53ef\u80fd\u662f\u4e2d\u95f4\u4eba\u653b\u51fb<br \/>\n            logSafeError(&#034;SSL Error&#034;, &#034;SSL handshake failed&#034;);<\/p>\n<p>            \/\/ \u63d0\u793a\u7528\u6237\u68c0\u67e5\u7f51\u7edc\u5b89\u5168\u6027<br \/>\n            showSecurityWarning(context);<\/p>\n<p>        } else if (exception instanceof UnknownHostException) {<br \/>\n            \/\/ DNS\u89e3\u6790\u5931\u8d25<br \/>\n            logSafeError(&#034;Network Error&#034;, &#034;Cannot resolve host&#034;);<\/p>\n<p>            showUserFriendlyError(<br \/>\n                context,<br \/>\n                context.getString(R.string.network_unavailable)<br \/>\n            );<\/p>\n<p>        } else {<br \/>\n            \/\/ \u901a\u7528\u9519\u8bef\u5904\u7406<br \/>\n            logSafeError(&#034;Unknown Error&#034;, &#034;An unexpected error occurred&#034;);<\/p>\n<p>            showUserFriendlyError(<br \/>\n                context,<br \/>\n                context.getString(R.string.generic_error)<br \/>\n            );<br \/>\n        }<br \/>\n    }<\/p>\n<p>    private static void logSafeError(String type, Object details) {<br \/>\n        \/\/ \u5b89\u5168\u7684\u65e5\u5fd7\u8bb0\u5f55&#xff0c;\u907f\u514d\u6cc4\u9732\u654f\u611f\u4fe1\u606f<br \/>\n        Map&lt;String, Object&gt; safeDetails &#061; new HashMap&lt;&gt;();<br \/>\n        safeDetails.put(&#034;type&#034;, type);<br \/>\n        safeDetails.put(&#034;details&#034;, details);<br \/>\n        safeDetails.put(&#034;timestamp&#034;, System.currentTimeMillis());<br \/>\n        safeDetails.put(&#034;app_version&#034;, BuildConfig.VERSION_NAME);<\/p>\n<p>        \/\/ \u79fb\u9664\u53ef\u80fd\u7684\u654f\u611f\u4fe1\u606f<br \/>\n        if (details instanceof String) {<br \/>\n            String detailString &#061; (String) details;<br \/>\n            detailString &#061; sanitizeString(detailString);<br \/>\n            safeDetails.put(&#034;details&#034;, detailString);<br \/>\n        }<\/p>\n<p>        Log.e(TAG, safeDetails.toString());<\/p>\n<p>        \/\/ \u53d1\u9001\u5230\u5b89\u5168\u7684\u9519\u8bef\u6536\u96c6\u670d\u52a1<br \/>\n        sendToErrorReporting(safeDetails);<br \/>\n    }<\/p>\n<p>    private static String sanitizeString(String input) {<br \/>\n        \/\/ \u79fb\u9664\u53ef\u80fd\u654f\u611f\u7684\u4fe1\u606f<br \/>\n        return input<br \/>\n            .replaceAll(&#034;(?i)password&#061;[^&amp;]*&#034;, &#034;password&#061;***&#034;)<br \/>\n            .replaceAll(&#034;(?i)token&#061;[^&amp;]*&#034;, &#034;token&#061;***&#034;)<br \/>\n            .replaceAll(&#034;(?i)api_key&#061;[^&amp;]*&#034;, &#034;api_key&#061;***&#034;)<br \/>\n            .replaceAll(&#034;\\\\\\\\d{4}-\\\\\\\\d{4}-\\\\\\\\d{4}-\\\\\\\\d{4}&#034;, &#034;****-****-****-****&#034;) \/\/ \u4fe1\u7528\u5361<br \/>\n            .replaceAll(&#034;\\\\\\\\b[A-Za-z0-9._%&#043;-]&#043;&#064;[A-Za-z0-9.-]&#043;\\\\\\\\.[A-Z|a-z]{2,}\\\\\\\\b&#034;, &#034;***&#064;***.***&#034;); \/\/ \u90ae\u7bb1<br \/>\n    }<\/p>\n<p>    private static String getSafeUserMessage(Context context, int statusCode) {<br \/>\n        \/\/ \u8fd4\u56de\u7528\u6237\u53cb\u597d\u7684\u9519\u8bef\u6d88\u606f&#xff0c;\u4e0d\u5305\u542b\u6280\u672f\u7ec6\u8282<br \/>\n        switch (statusCode) {<br \/>\n            case 400:<br \/>\n                return context.getString(R.string.error_bad_request);<br \/>\n            case 401:<br \/>\n            case 403:<br \/>\n                return context.getString(R.string.error_authentication);<br \/>\n            case 404:<br \/>\n                return context.getString(R.string.error_not_found);<br \/>\n            case 408:<br \/>\n            case 504:<br \/>\n                return context.getString(R.string.error_timeout);<br \/>\n            case 429:<br \/>\n                return context.getString(R.string.error_too_many_requests);<br \/>\n            case 500:<br \/>\n            case 502:<br \/>\n            case 503:<br \/>\n                return context.getString(R.string.error_server);<br \/>\n            default:<br \/>\n                return context.getString(R.string.error_generic);<br \/>\n        }<br \/>\n    }<\/p>\n<p>    private static void showUserFriendlyError(Context context, String message) {<br \/>\n        \/\/ \u5728\u4e3b\u7ebf\u7a0b\u663e\u793a\u9519\u8bef<br \/>\n        new Handler(Looper.getMainLooper()).post(() -&gt; {<br \/>\n            Toast.makeText(context, message, Toast.LENGTH_LONG).show();<br \/>\n        });<br \/>\n    }<\/p>\n<p>    private static void showSecurityWarning(Context context) {<br \/>\n        \/\/ \u663e\u793a\u5b89\u5168\u8b66\u544a\u5bf9\u8bdd\u6846<br \/>\n        new Handler(Looper.getMainLooper()).post(() -&gt; {<br \/>\n            new AlertDialog.Builder(context)<br \/>\n                .setTitle(R.string.security_warning_title)<br \/>\n                .setMessage(R.string.security_warning_message)<br \/>\n                .setPositiveButton(R.string.ok, null)<br \/>\n                .show();<br \/>\n        });<br \/>\n    }<br \/>\n}<\/p>\n<h3>32.6 \u79fb\u52a8\u7aef\u8c03\u8bd5\u4e0e\u76d1\u63a7<\/h3>\n<h4>32.6.1 \u7f51\u7edc\u8bf7\u6c42\u8c03\u8bd5\u5de5\u5177<\/h4>\n<p>javascript<\/p>\n<p>\/\/ React Native\u7f51\u7edc\u8c03\u8bd5\u5668<br \/>\nclass NetworkDebugger {<br \/>\n  constructor() {<br \/>\n    this.requests &#061; [];<br \/>\n    this.maxRequests &#061; 1000;<\/p>\n<p>    \/\/ \u62e6\u622a\u6240\u6709fetch\u8bf7\u6c42<br \/>\n    this.originalFetch &#061; global.fetch;<br \/>\n    this.setupInterceptor();<br \/>\n  }<\/p>\n<p>  setupInterceptor() {<br \/>\n    global.fetch &#061; async (url, options &#061; {}) &#061;&gt; {<br \/>\n      const startTime &#061; Date.now();<br \/>\n      const requestId &#061; uuid.v4();<\/p>\n<p>      const requestInfo &#061; {<br \/>\n        id: requestId,<br \/>\n        url,<br \/>\n        method: options.method || &#039;GET&#039;,<br \/>\n        headers: options.headers || {},<br \/>\n        body: options.body,<br \/>\n        startTime,<br \/>\n        status: &#039;pending&#039;<br \/>\n      };<\/p>\n<p>      this.addRequest(requestInfo);<\/p>\n<p>      try {<br \/>\n        const response &#061; await this.originalFetch(url, options);<br \/>\n        const endTime &#061; Date.now();<\/p>\n<p>        const responseInfo &#061; {<br \/>\n          &#8230;requestInfo,<br \/>\n          endTime,<br \/>\n          duration: endTime &#8211; startTime,<br \/>\n          statusCode: response.status,<br \/>\n          statusText: response.statusText,<br \/>\n          headers: Object.fromEntries(response.headers.entries()),<br \/>\n          status: &#039;completed&#039;<br \/>\n        };<\/p>\n<p>        \/\/ \u514b\u9686\u54cd\u5e94\u4ee5\u4fbf\u8bfb\u53d6body<br \/>\n        const clonedResponse &#061; response.clone();<\/p>\n<p>        \/\/ \u5c1d\u8bd5\u8bfb\u53d6\u54cd\u5e94\u4f53<br \/>\n        try {<br \/>\n          const body &#061; await clonedResponse.text();<br \/>\n          responseInfo.body &#061; this.sanitizeBody(body);<br \/>\n        } catch (error) {<br \/>\n          responseInfo.bodyError &#061; &#039;Unable to read response body&#039;;<br \/>\n        }<\/p>\n<p>        this.updateRequest(requestId, responseInfo);<\/p>\n<p>        \/\/ \u89e6\u53d1\u4e8b\u4ef6<br \/>\n        this.emit(&#039;request_completed&#039;, responseInfo);<\/p>\n<p>        return response;<\/p>\n<p>      } catch (error) {<br \/>\n        const endTime &#061; Date.now();<\/p>\n<p>        const errorInfo &#061; {<br \/>\n          &#8230;requestInfo,<br \/>\n          endTime,<br \/>\n          duration: endTime &#8211; startTime,<br \/>\n          error: error.message,<br \/>\n          status: &#039;failed&#039;<br \/>\n        };<\/p>\n<p>        this.updateRequest(requestId, errorInfo);<\/p>\n<p>        \/\/ \u89e6\u53d1\u4e8b\u4ef6<br \/>\n        this.emit(&#039;request_failed&#039;, errorInfo);<\/p>\n<p>        throw error;<br \/>\n      }<br \/>\n    };<br \/>\n  }<\/p>\n<p>  addRequest(request) {<br \/>\n    this.requests.unshift(request);<\/p>\n<p>    \/\/ \u9650\u5236\u8bf7\u6c42\u6570\u91cf<br \/>\n    if (this.requests.length &gt; this.maxRequests) {<br \/>\n      this.requests &#061; this.requests.slice(0, this.maxRequests);<br \/>\n    }<\/p>\n<p>    \/\/ \u5b58\u50a8\u5230AsyncStorage\u4ee5\u4fbf\u79bb\u7ebf\u67e5\u770b<br \/>\n    AsyncStorage.setItem(<br \/>\n      &#039;network_debug_logs&#039;,<br \/>\n      JSON.stringify(this.requests.slice(0, 100))<br \/>\n    );<br \/>\n  }<\/p>\n<p>  updateRequest(id, updates) {<br \/>\n    const index &#061; this.requests.findIndex(req &#061;&gt; req.id &#061;&#061;&#061; id);<br \/>\n    if (index !&#061;&#061; -1) {<br \/>\n      this.requests[index] &#061; { &#8230;this.requests[index], &#8230;updates };<br \/>\n    }<br \/>\n  }<\/p>\n<p>  sanitizeBody(body) {<br \/>\n    \/\/ \u79fb\u9664\u654f\u611f\u4fe1\u606f<br \/>\n    try {<br \/>\n      const parsed &#061; JSON.parse(body);<br \/>\n      return this.sanitizeObject(parsed);<br \/>\n    } catch {<br \/>\n      \/\/ \u5982\u679c\u4e0d\u662fJSON&#xff0c;\u8fd4\u56de\u539f\u5b57\u7b26\u4e32<br \/>\n      return body.replace(\/password&#061;[^&amp;]*\/gi, &#039;password&#061;***&#039;)<br \/>\n                .replace(\/token&#061;[^&amp;]*\/gi, &#039;token&#061;***&#039;);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  sanitizeObject(obj) {<br \/>\n    const sensitiveKeys &#061; [&#039;password&#039;, &#039;token&#039;, &#039;secret&#039;, &#039;key&#039;, &#039;auth&#039;];<\/p>\n<p>    if (Array.isArray(obj)) {<br \/>\n      return obj.map(item &#061;&gt; this.sanitizeObject(item));<br \/>\n    }<\/p>\n<p>    if (obj !&#061;&#061; null &amp;&amp; typeof obj &#061;&#061;&#061; &#039;object&#039;) {<br \/>\n      const sanitized &#061; {};<\/p>\n<p>      for (const [key, value] of Object.entries(obj)) {<br \/>\n        if (sensitiveKeys.some(sensitive &#061;&gt;<br \/>\n          key.toLowerCase().includes(sensitive))) {<br \/>\n          sanitized[key] &#061; &#039;***&#039;;<br \/>\n        } else if (typeof value &#061;&#061;&#061; &#039;object&#039;) {<br \/>\n          sanitized[key] &#061; this.sanitizeObject(value);<br \/>\n        } else {<br \/>\n          sanitized[key] &#061; value;<br \/>\n        }<br \/>\n      }<\/p>\n<p>      return sanitized;<br \/>\n    }<\/p>\n<p>    return obj;<br \/>\n  }<\/p>\n<p>  getStats() {<br \/>\n    const stats &#061; {<br \/>\n      total: this.requests.length,<br \/>\n      success: this.requests.filter(r &#061;&gt; r.status &#061;&#061;&#061; &#039;completed&#039;).length,<br \/>\n      failed: this.requests.filter(r &#061;&gt; r.status &#061;&#061;&#061; &#039;failed&#039;).length,<br \/>\n      pending: this.requests.filter(r &#061;&gt; r.status &#061;&#061;&#061; &#039;pending&#039;).length,<\/p>\n<p>      statusCodes: {},<br \/>\n      averageDuration: 0,<br \/>\n      byEndpoint: {}<br \/>\n    };<\/p>\n<p>    \/\/ \u7edf\u8ba1\u72b6\u6001\u7801\u5206\u5e03<br \/>\n    this.requests.forEach(request &#061;&gt; {<br \/>\n      if (request.statusCode) {<br \/>\n        const code &#061; request.statusCode;<br \/>\n        stats.statusCodes[code] &#061; (stats.statusCodes[code] || 0) &#043; 1;<br \/>\n      }<\/p>\n<p>      \/\/ \u7edf\u8ba1\u63a5\u53e3\u8017\u65f6<br \/>\n      if (request.duration) {<br \/>\n        stats.totalDuration &#061; (stats.totalDuration || 0) &#043; request.duration;<br \/>\n      }<\/p>\n<p>      \/\/ \u6309\u7aef\u70b9\u7edf\u8ba1<br \/>\n      if (request.url) {<br \/>\n        const endpoint &#061; this.extractEndpoint(request.url);<br \/>\n        if (!stats.byEndpoint[endpoint]) {<br \/>\n          stats.byEndpoint[endpoint] &#061; {<br \/>\n            count: 0,<br \/>\n            success: 0,<br \/>\n            failed: 0,<br \/>\n            totalDuration: 0<br \/>\n          };<br \/>\n        }<\/p>\n<p>        stats.byEndpoint[endpoint].count&#043;&#043;;<br \/>\n        if (request.status &#061;&#061;&#061; &#039;completed&#039;) {<br \/>\n          stats.byEndpoint[endpoint].success&#043;&#043;;<br \/>\n        } else if (request.status &#061;&#061;&#061; &#039;failed&#039;) {<br \/>\n          stats.byEndpoint[endpoint].failed&#043;&#043;;<br \/>\n        }<\/p>\n<p>        if (request.duration) {<br \/>\n          stats.byEndpoint[endpoint].totalDuration &#043;&#061; request.duration;<br \/>\n        }<br \/>\n      }<br \/>\n    });<\/p>\n<p>    if (stats.success &gt; 0) {<br \/>\n      stats.averageDuration &#061; stats.totalDuration \/ stats.success;<br \/>\n    }<\/p>\n<p>    return stats;<br \/>\n  }<\/p>\n<p>  extractEndpoint(url) {<br \/>\n    try {<br \/>\n      const urlObj &#061; new URL(url);<br \/>\n      return urlObj.pathname;<br \/>\n    } catch {<br \/>\n      return url;<br \/>\n    }<br \/>\n  }<\/p>\n<p>  \/\/ \u5bfc\u51fa\u4e3acURL\u547d\u4ee4&#xff0c;\u4fbf\u4e8e\u8c03\u8bd5<br \/>\n  exportAsCurl(requestId) {<br \/>\n    const request &#061; this.requests.find(req &#061;&gt; req.id &#061;&#061;&#061; requestId);<br \/>\n    if (!request) return &#039;&#039;;<\/p>\n<p>    let curl &#061; &#096;curl -X ${request.method} &#039;${request.url}&#039;&#096;;<\/p>\n<p>    \/\/ \u6dfb\u52a0headers<br \/>\n    Object.entries(request.headers || {}).forEach(([key, value]) &#061;&gt; {<br \/>\n      curl &#043;&#061; &#096; -H &#039;${key}: ${value}&#039;&#096;;<br \/>\n    });<\/p>\n<p>    \/\/ \u6dfb\u52a0body<br \/>\n    if (request.body) {<br \/>\n      curl &#043;&#061; &#096; -d &#039;${request.body}&#039;&#096;;<br \/>\n    }<\/p>\n<p>    return curl;<br \/>\n  }<br \/>\n}<\/p>\n<h3>32.7 \u8de8\u5e73\u53f0\u72b6\u6001\u7801\u5904\u7406<\/h3>\n<h4>32.7.1 React Native\u7edf\u4e00\u7f51\u7edc\u5c42<\/h4>\n<p>typescript<\/p>\n<p>\/\/ React Native\u7edf\u4e00\u7f51\u7edc\u5ba2\u6237\u7aef<br \/>\nclass RNHttpClient {<br \/>\n  private baseURL: string;<br \/>\n  private timeout: number;<br \/>\n  private retryCount: number;<br \/>\n  private interceptors: Interceptor[];<\/p>\n<p>  constructor(config: HttpClientConfig) {<br \/>\n    this.baseURL &#061; config.baseURL;<br \/>\n    this.timeout &#061; config.timeout || 30000;<br \/>\n    this.retryCount &#061; config.retryCount || 3;<br \/>\n    this.interceptors &#061; config.interceptors || [];<br \/>\n  }<\/p>\n<p>  async request&lt;T&gt;(<br \/>\n    endpoint: string,<br \/>\n    options: RequestOptions &#061; {}<br \/>\n  ): Promise&lt;ApiResponse&lt;T&gt;&gt; {<br \/>\n    const {<br \/>\n      method &#061; &#039;GET&#039;,<br \/>\n      data,<br \/>\n      headers &#061; {},<br \/>\n      retry &#061; this.retryCount,<br \/>\n      timeout &#061; this.timeout,<br \/>\n      responseType &#061; &#039;json&#039;<br \/>\n    } &#061; options;<\/p>\n<p>    const url &#061; &#096;${this.baseURL}${endpoint}&#096;;<\/p>\n<p>    \/\/ \u6267\u884c\u8bf7\u6c42\u62e6\u622a\u5668<br \/>\n    let requestConfig &#061; { method, url, headers, data, timeout };<br \/>\n    for (const interceptor of this.interceptors) {<br \/>\n      if (interceptor.request) {<br \/>\n        requestConfig &#061; await interceptor.request(requestConfig);<br \/>\n      }<br \/>\n    }<\/p>\n<p>    const controller &#061; new AbortController();<br \/>\n    const timeoutId &#061; setTimeout(() &#061;&gt; controller.abort(), timeout);<\/p>\n<p>    try {<br \/>\n      const fetchOptions: RequestInit &#061; {<br \/>\n        method: requestConfig.method,<br \/>\n        headers: {<br \/>\n          &#039;Content-Type&#039;: &#039;application\/json&#039;,<br \/>\n          &#8230;requestConfig.headers,<br \/>\n        },<br \/>\n        signal: controller.signal,<br \/>\n      };<\/p>\n<p>      if (data &amp;&amp; method !&#061;&#061; &#039;GET&#039; &amp;&amp; method !&#061;&#061; &#039;HEAD&#039;) {<br \/>\n        fetchOptions.body &#061; JSON.stringify(data);<br \/>\n      }<\/p>\n<p>      const response &#061; await fetch(requestConfig.url, fetchOptions);<br \/>\n      clearTimeout(timeoutId);<\/p>\n<p>      \/\/ \u6267\u884c\u54cd\u5e94\u62e6\u622a\u5668<br \/>\n      let processedResponse &#061; response;<br \/>\n      for (const interceptor of this.interceptors) {<br \/>\n        if (interceptor.response) {<br \/>\n          processedResponse &#061; await interceptor.response(processedResponse);<br \/>\n        }<br \/>\n      }<\/p>\n<p>      \/\/ \u5904\u7406\u54cd\u5e94<br \/>\n      const result &#061; await this.handleResponse&lt;T&gt;(<br \/>\n        processedResponse,<br \/>\n        responseType<br \/>\n      );<\/p>\n<p>      \/\/ \u68c0\u67e5\u662f\u5426\u9700\u8981\u91cd\u8bd5<br \/>\n      if (this.shouldRetry(result.status, retry)) {<br \/>\n        return this.retryRequest(endpoint, options, retry &#8211; 1);<br \/>\n      }<\/p>\n<p>      return result;<\/p>\n<p>    } catch (error) {<br \/>\n      clearTimeout(timeoutId);<\/p>\n<p>      \/\/ \u7f51\u7edc\u9519\u8bef&#xff0c;\u6839\u636e\u9519\u8bef\u7c7b\u578b\u51b3\u5b9a\u662f\u5426\u91cd\u8bd5<br \/>\n      if (this.isNetworkErrorRetryable(error) &amp;&amp; retry &gt; 0) {<br \/>\n        return this.retryRequest(endpoint, options, retry &#8211; 1);<br \/>\n      }<\/p>\n<p>      throw this.normalizeError(error);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  private async handleResponse&lt;T&gt;(<br \/>\n    response: Response,<br \/>\n    responseType: ResponseType<br \/>\n  ): Promise&lt;ApiResponse&lt;T&gt;&gt; {<br \/>\n    const status &#061; response.status;<br \/>\n    const headers &#061; Object.fromEntries(response.headers.entries());<\/p>\n<p>    let data: any;<br \/>\n    let error: any;<\/p>\n<p>    try {<br \/>\n      if (status &gt;&#061; 200 &amp;&amp; status &lt; 300) {<br \/>\n        \/\/ \u6210\u529f\u54cd\u5e94<br \/>\n        switch (responseType) {<br \/>\n          case &#039;json&#039;:<br \/>\n            data &#061; await response.json();<br \/>\n            break;<br \/>\n          case &#039;text&#039;:<br \/>\n            data &#061; await response.text();<br \/>\n            break;<br \/>\n          case &#039;blob&#039;:<br \/>\n            data &#061; await response.blob();<br \/>\n            break;<br \/>\n          case &#039;arraybuffer&#039;:<br \/>\n            data &#061; await response.arrayBuffer();<br \/>\n            break;<br \/>\n          default:<br \/>\n            data &#061; await response.json();<br \/>\n        }<br \/>\n      } else {<br \/>\n        \/\/ \u9519\u8bef\u54cd\u5e94<br \/>\n        error &#061; await this.parseError(response);<br \/>\n      }<br \/>\n    } catch (parseError) {<br \/>\n      error &#061; {<br \/>\n        code: &#039;PARSE_ERROR&#039;,<br \/>\n        message: &#039;Failed to parse response&#039;,<br \/>\n        originalError: parseError,<br \/>\n      };<br \/>\n    }<\/p>\n<p>    return {<br \/>\n      status,<br \/>\n      headers,<br \/>\n      data,<br \/>\n      error,<br \/>\n      ok: status &gt;&#061; 200 &amp;&amp; status &lt; 300,<br \/>\n    };<br \/>\n  }<\/p>\n<p>  private async parseError(response: Response): Promise&lt;ApiError&gt; {<br \/>\n    try {<br \/>\n      const errorData &#061; await response.json();<\/p>\n<p>      \/\/ \u6807\u51c6\u5316\u9519\u8bef\u683c\u5f0f<br \/>\n      return {<br \/>\n        code: errorData.code || &#096;HTTP_${response.status}&#096;,<br \/>\n        message: errorData.message || response.statusText,<br \/>\n        details: errorData.details,<br \/>\n        status: response.status,<br \/>\n        timestamp: new Date().toISOString(),<br \/>\n      };<br \/>\n    } catch {<br \/>\n      \/\/ \u5982\u679c\u65e0\u6cd5\u89e3\u6790\u4e3aJSON&#xff0c;\u8fd4\u56de\u901a\u7528\u9519\u8bef<br \/>\n      return {<br \/>\n        code: &#096;HTTP_${response.status}&#096;,<br \/>\n        message: response.statusText,<br \/>\n        status: response.status,<br \/>\n        timestamp: new Date().toISOString(),<br \/>\n      };<br \/>\n    }<br \/>\n  }<\/p>\n<p>  private shouldRetry(status: number, retryCount: number): boolean {<br \/>\n    if (retryCount &lt;&#061; 0) return false;<\/p>\n<p>    \/\/ \u4ee5\u4e0b\u72b6\u6001\u7801\u9700\u8981\u91cd\u8bd5<br \/>\n    const retryStatusCodes &#061; [408, 429, 500, 502, 503, 504];<\/p>\n<p>    \/\/ \u5982\u679c\u662f5xx\u9519\u8bef\u6216\u8005\u7279\u5b9a4xx\u9519\u8bef&#xff0c;\u4e14\u8fd8\u6709\u91cd\u8bd5\u6b21\u6570&#xff0c;\u5219\u91cd\u8bd5<br \/>\n    return retryStatusCodes.includes(status);<br \/>\n  }<\/p>\n<p>  private isNetworkErrorRetryable(error: any): boolean {<br \/>\n    \/\/ \u7f51\u7edc\u9519\u8bef\u901a\u5e38\u53ef\u4ee5\u91cd\u8bd5<br \/>\n    const retryableErrors &#061; [<br \/>\n      &#039;AbortError&#039;, \/\/ \u8d85\u65f6<br \/>\n      &#039;TypeError&#039;, \/\/ \u7f51\u7edc\u9519\u8bef<br \/>\n      &#039;Network request failed&#039;,<br \/>\n    ];<\/p>\n<p>    return (<br \/>\n      error.name &#061;&#061;&#061; &#039;AbortError&#039; ||<br \/>\n      retryableErrors.some(msg &#061;&gt; error.message?.includes(msg))<br \/>\n    );<br \/>\n  }<\/p>\n<p>  private async retryRequest&lt;T&gt;(<br \/>\n    endpoint: string,<br \/>\n    options: RequestOptions,<br \/>\n    retryCount: number<br \/>\n  ): Promise&lt;ApiResponse&lt;T&gt;&gt; {<br \/>\n    \/\/ \u6307\u6570\u9000\u907f<br \/>\n    const delay &#061; Math.pow(2, this.retryCount &#8211; retryCount) * 1000;<\/p>\n<p>    await new Promise(resolve &#061;&gt; setTimeout(resolve, delay));<\/p>\n<p>    return this.request(endpoint, { &#8230;options, retry: retryCount });<br \/>\n  }<\/p>\n<p>  private normalizeError(error: any): ApiError {<br \/>\n    if (error.name &#061;&#061;&#061; &#039;AbortError&#039;) {<br \/>\n      return {<br \/>\n        code: &#039;TIMEOUT&#039;,<br \/>\n        message: &#039;Request timeout&#039;,<br \/>\n        originalError: error,<br \/>\n      };<br \/>\n    }<\/p>\n<p>    if (error.message?.includes(&#039;Network request failed&#039;)) {<br \/>\n      return {<br \/>\n        code: &#039;NETWORK_ERROR&#039;,<br \/>\n        message: &#039;Network connection failed&#039;,<br \/>\n        originalError: error,<br \/>\n      };<br \/>\n    }<\/p>\n<p>    return {<br \/>\n      code: &#039;UNKNOWN_ERROR&#039;,<br \/>\n      message: error.message || &#039;An unknown error occurred&#039;,<br \/>\n      originalError: error,<br \/>\n    };<br \/>\n  }<\/p>\n<p>  \/\/ \u6dfb\u52a0\u62e6\u622a\u5668<br \/>\n  use(interceptor: Interceptor): void {<br \/>\n    this.interceptors.push(interceptor);<br \/>\n  }<\/p>\n<p>  \/\/ \u79fb\u9664\u62e6\u622a\u5668<br \/>\n  eject(interceptor: Interceptor): void {<br \/>\n    const index &#061; this.interceptors.indexOf(interceptor);<br \/>\n    if (index !&#061;&#061; -1) {<br \/>\n      this.interceptors.splice(index, 1);<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ \u4f7f\u7528\u793a\u4f8b<br \/>\nconst httpClient &#061; new RNHttpClient({<br \/>\n  baseURL: &#039;https:\/\/api.example.com&#039;,<br \/>\n  timeout: 30000,<br \/>\n  retryCount: 3,<br \/>\n});<\/p>\n<p>\/\/ \u6dfb\u52a0\u8ba4\u8bc1\u62e6\u622a\u5668<br \/>\nhttpClient.use({<br \/>\n  request: async (config) &#061;&gt; {<br \/>\n    const token &#061; await AsyncStorage.getItem(&#039;access_token&#039;);<br \/>\n    if (token) {<br \/>\n      config.headers.Authorization &#061; &#096;Bearer ${token}&#096;;<br \/>\n    }<br \/>\n    return config;<br \/>\n  },<\/p>\n<p>  response: async (response) &#061;&gt; {<br \/>\n    \/\/ \u68c0\u67e5token\u662f\u5426\u8fc7\u671f<br \/>\n    if (response.status &#061;&#061;&#061; 401) {<br \/>\n      \/\/ \u5c1d\u8bd5\u5237\u65b0token<br \/>\n      const newToken &#061; await refreshToken();<br \/>\n      if (newToken) {<br \/>\n        \/\/ \u66f4\u65b0\u5b58\u50a8<br \/>\n        await AsyncStorage.setItem(&#039;access_token&#039;, newToken);<br \/>\n        \/\/ \u53ef\u4ee5\u5728\u8fd9\u91cc\u91cd\u65b0\u53d1\u9001\u539f\u59cb\u8bf7\u6c42<br \/>\n      }<br \/>\n    }<br \/>\n    return response;<br \/>\n  },<br \/>\n});<\/p>\n<p>\/\/ \u6dfb\u52a0\u65e5\u5fd7\u62e6\u622a\u5668<br \/>\nhttpClient.use({<br \/>\n  request: async (config) &#061;&gt; {<br \/>\n    console.log(&#039;Request:&#039;, {<br \/>\n      url: config.url,<br \/>\n      method: config.method,<br \/>\n      headers: config.headers,<br \/>\n    });<br \/>\n    return config;<br \/>\n  },<\/p>\n<p>  response: async (response) &#061;&gt; {<br \/>\n    console.log(&#039;Response:&#039;, {<br \/>\n      status: response.status,<br \/>\n      statusText: response.statusText,<br \/>\n      headers: Object.fromEntries(response.headers.entries()),<br \/>\n    });<br \/>\n    return response;<br \/>\n  },<br \/>\n});<\/p>\n<h3>32.8 \u79fb\u52a8\u7aef\u6d4b\u8bd5\u7b56\u7565<\/h3>\n<h4>32.8.1 \u7f51\u7edc\u72b6\u6001\u6a21\u62df\u6d4b\u8bd5<\/h4>\n<p>typescript<\/p>\n<p>\/\/ React Native\u7f51\u7edc\u6d4b\u8bd5\u5de5\u5177<br \/>\ndescribe(&#039;Network Handling&#039;, () &#061;&gt; {<br \/>\n  let httpClient: RNHttpClient;<\/p>\n<p>  beforeEach(() &#061;&gt; {<br \/>\n    httpClient &#061; new RNHttpClient({<br \/>\n      baseURL: &#039;https:\/\/api.example.com&#039;,<br \/>\n    });<br \/>\n  });<\/p>\n<p>  test(&#039;should handle 401 Unauthorized gracefully&#039;, async () &#061;&gt; {<br \/>\n    \/\/ \u6a21\u62df401\u54cd\u5e94<br \/>\n    fetchMock.mockResponseOnce(&#039;&#039;, { status: 401 });<\/p>\n<p>    const response &#061; await httpClient.request(&#039;\/protected&#039;);<\/p>\n<p>    expect(response.status).toBe(401);<br \/>\n    expect(response.error?.code).toBe(&#039;HTTP_401&#039;);<br \/>\n    expect(response.ok).toBe(false);<br \/>\n  });<\/p>\n<p>  test(&#039;should retry on 500 error&#039;, async () &#061;&gt; {<br \/>\n    \/\/ \u6a21\u62df\u7b2c\u4e00\u6b21500&#xff0c;\u7b2c\u4e8c\u6b21\u6210\u529f<br \/>\n    fetchMock<br \/>\n      .mockResponseOnce(&#039;&#039;, { status: 500 })<br \/>\n      .mockResponseOnce(JSON.stringify({ success: true }));<\/p>\n<p>    const response &#061; await httpClient.request(&#039;\/unstable&#039;, { retry: 2 });<\/p>\n<p>    expect(response.status).toBe(200);<br \/>\n    expect(response.data).toEqual({ success: true });<br \/>\n    expect(fetchMock).toHaveBeenCalledTimes(2);<br \/>\n  });<\/p>\n<p>  test(&#039;should handle offline mode&#039;, async () &#061;&gt; {<br \/>\n    \/\/ \u6a21\u62df\u7f51\u7edc\u9519\u8bef<br \/>\n    fetchMock.mockReject(new Error(&#039;Network request failed&#039;));<\/p>\n<p>    await expect(httpClient.request(&#039;\/data&#039;)).rejects.toMatchObject({<br \/>\n      code: &#039;NETWORK_ERROR&#039;,<br \/>\n    });<br \/>\n  });<\/p>\n<p>  test(&#039;should timeout after specified time&#039;, async () &#061;&gt; {<br \/>\n    \/\/ \u6a21\u62df\u957f\u65f6\u95f4\u8bf7\u6c42<br \/>\n    fetchMock.mockResponse(async () &#061;&gt; {<br \/>\n      await new Promise(resolve &#061;&gt; setTimeout(resolve, 40000));<br \/>\n      return { body: &#039;OK&#039; };<br \/>\n    });<\/p>\n<p>    const response &#061; httpClient.request(&#039;\/slow&#039;, { timeout: 1000 });<\/p>\n<p>    await expect(response).rejects.toMatchObject({<br \/>\n      code: &#039;TIMEOUT&#039;,<br \/>\n    });<br \/>\n  });<br \/>\n});<\/p>\n<p>\/\/ \u7f51\u7edc\u72b6\u6001\u5207\u6362\u6d4b\u8bd5<br \/>\ndescribe(&#039;Network State Changes&#039;, () &#061;&gt; {<br \/>\n  test(&#039;should adapt to cellular network&#039;, async () &#061;&gt; {<br \/>\n    const imageLoader &#061; new SmartImageLoader();<\/p>\n<p>    \/\/ \u6a21\u62df\u5207\u6362\u5230\u8702\u7a9d\u7f51\u7edc<br \/>\n    NetworkMonitor.setNetworkType(&#039;cellular&#039;);<\/p>\n<p>    const url &#061; new URL(&#039;https:\/\/example.com\/image.jpg&#039;);<br \/>\n    const optimalURL &#061; imageLoader.optimalImageURL(url, &#039;cellular&#039;);<\/p>\n<p>    expect(optimalURL.searchParams.get(&#039;quality&#039;)).toBe(&#039;medium&#039;);<br \/>\n    expect(optimalURL.searchParams.get(&#039;width&#039;)).toBe(&#039;720&#039;);<br \/>\n  });<\/p>\n<p>  test(&#039;should use cached data when offline&#039;, async () &#061;&gt; {<br \/>\n    const repository &#061; new OfflineFirstRepository();<\/p>\n<p>    \/\/ \u9884\u52a0\u8f7d\u7f13\u5b58<br \/>\n    await repository.local.save({<br \/>\n      key: &#039;test-data&#039;,<br \/>\n      data: { cached: true },<br \/>\n      timestamp: Date.now(),<br \/>\n    });<\/p>\n<p>    \/\/ \u6a21\u62df\u79bb\u7ebf\u72b6\u6001<br \/>\n    NetworkMonitor.setConnected(false);<\/p>\n<p>    const result &#061; await repository.fetchData({<br \/>\n      key: &#039;test-data&#039;,<br \/>\n      remoteFetch: () &#061;&gt; Promise.resolve({ fresh: true }),<br \/>\n    });<\/p>\n<p>    expect(result.source).toBe(&#039;cache&#039;);<br \/>\n    expect(result.data).toEqual({ cached: true });<br \/>\n    expect(result.isStale).toBe(true);<br \/>\n  });<br \/>\n});<\/p>\n<p>\/\/ \u6027\u80fd\u6d4b\u8bd5<br \/>\ndescribe(&#039;Network Performance&#039;, () &#061;&gt; {<br \/>\n  test(&#039;should not exceed maximum concurrent requests&#039;, async () &#061;&gt; {<br \/>\n    const loader &#061; new SmartImageLoader();<\/p>\n<p>    \/\/ \u540c\u65f6\u53d1\u8d77\u591a\u4e2a\u8bf7\u6c42<br \/>\n    const promises &#061; Array(10)<br \/>\n      .fill(0)<br \/>\n      .map((_, i) &#061;&gt;<br \/>\n        loader.loadImage(<br \/>\n          new URL(&#096;https:\/\/example.com\/image${i}.jpg&#096;),<br \/>\n          new UIImageView()<br \/>\n        )<br \/>\n      );<\/p>\n<p>    \/\/ \u68c0\u67e5\u5e76\u53d1\u6570<br \/>\n    expect(loader.activeRequests).toBeLessThanOrEqual(loader.maxConcurrentDownloads);<\/p>\n<p>    await Promise.all(promises);<br \/>\n  });<\/p>\n<p>  test(&#039;should cache images appropriately&#039;, async () &#061;&gt; {<br \/>\n    const loader &#061; new SmartImageLoader();<br \/>\n    const imageView &#061; new UIImageView();<br \/>\n    const url &#061; new URL(&#039;https:\/\/example.com\/test.jpg&#039;);<\/p>\n<p>    \/\/ \u7b2c\u4e00\u6b21\u52a0\u8f7d<br \/>\n    await loader.loadImage(url, imageView);<br \/>\n    expect(loader.memoryCache.object(forKey: url.absoluteString)).toBeDefined();<\/p>\n<p>    \/\/ \u7b2c\u4e8c\u6b21\u5e94\u8be5\u4ece\u7f13\u5b58\u52a0\u8f7d<br \/>\n    const startTime &#061; Date.now();<br \/>\n    await loader.loadImage(url, imageView);<br \/>\n    const endTime &#061; Date.now();<\/p>\n<p>    \/\/ \u7f13\u5b58\u52a0\u8f7d\u5e94\u8be5\u5f88\u5feb<br \/>\n    expect(endTime &#8211; startTime).toBeLessThan(10);<br \/>\n  });<br \/>\n});<\/p>\n<h3>32.9 \u79fb\u52a8\u7aef\u6700\u4f73\u5b9e\u8df5\u603b\u7ed3<\/h3>\n<h4>32.9.1 \u5173\u952e\u539f\u5219<\/h4>\n<li>\n<p>\u4f18\u96c5\u964d\u7ea7<\/p>\n<ul>\n<li>\n<p>\u4f18\u5148\u63d0\u4f9b\u529f\u80fd&#xff0c;\u5373\u4f7f\u6570\u636e\u4e0d\u662f\u6700\u65b0\u7684<\/p>\n<\/li>\n<li>\n<p>\u7f13\u5b58\u4e0a\u6b21\u6210\u529f\u7684\u7ed3\u679c<\/p>\n<\/li>\n<li>\n<p>\u63d0\u4f9b\u79bb\u7ebf\u6a21\u5f0f<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u667a\u80fd\u91cd\u8bd5<\/p>\n<ul>\n<li>\n<p>\u6307\u6570\u9000\u907f\u7b97\u6cd5<\/p>\n<\/li>\n<li>\n<p>\u6839\u636e\u72b6\u6001\u7801\u51b3\u5b9a\u662f\u5426\u91cd\u8bd5<\/p>\n<\/li>\n<li>\n<p>\u8003\u8651\u7f51\u7edc\u7c7b\u578b\u8c03\u6574\u91cd\u8bd5\u7b56\u7565<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u7528\u6237\u4f53\u9a8c\u4f18\u5148<\/p>\n<ul>\n<li>\n<p>\u5feb\u901f\u54cd\u5e94\u7528\u6237\u64cd\u4f5c<\/p>\n<\/li>\n<li>\n<p>\u63d0\u4f9b\u6709\u610f\u4e49\u7684\u9519\u8bef\u63d0\u793a<\/p>\n<\/li>\n<li>\n<p>\u907f\u514d\u963b\u585eUI<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<h4>32.9.2 \u6280\u672f\u7b56\u7565<\/h4>\n<li>\n<p>\u7f51\u7edc\u4f18\u5316<\/p>\n<ul>\n<li>\n<p>\u538b\u7f29\u8bf7\u6c42\u548c\u54cd\u5e94<\/p>\n<\/li>\n<li>\n<p>\u6279\u91cf\u5904\u7406\u8bf7\u6c42<\/p>\n<\/li>\n<li>\n<p>\u9884\u52a0\u8f7d\u91cd\u8981\u6570\u636e<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u7f13\u5b58\u7b56\u7565<\/p>\n<ul>\n<li>\n<p>\u5185\u5b58\u7f13\u5b58 &#043; \u78c1\u76d8\u7f13\u5b58<\/p>\n<\/li>\n<li>\n<p>\u57fa\u4e8eTTL\u7684\u8fc7\u671f\u7b56\u7565<\/p>\n<\/li>\n<li>\n<p>\u79bb\u7ebf\u961f\u5217\u7ba1\u7406<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u9519\u8bef\u5904\u7406<\/p>\n<ul>\n<li>\n<p>\u533a\u5206\u53ef\u6062\u590d\u548c\u4e0d\u53ef\u6062\u590d\u9519\u8bef<\/p>\n<\/li>\n<li>\n<p>\u5b89\u5168\u7684\u9519\u8bef\u65e5\u5fd7\u8bb0\u5f55<\/p>\n<\/li>\n<li>\n<p>\u7528\u6237\u53cb\u597d\u7684\u9519\u8bef\u6d88\u606f<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<h4>32.9.3 \u76d1\u63a7\u6307\u6807<\/h4>\n<p>yaml<\/p>\n<p>\u79fb\u52a8\u5e94\u7528\u7f51\u7edc\u6027\u80fd\u6307\u6807:<br \/>\n  &#8211; \u8bf7\u6c42\u6210\u529f\u7387: &gt; 99%<br \/>\n  &#8211; \u5e73\u5747\u54cd\u5e94\u65f6\u95f4: &lt; 2\u79d2<br \/>\n  &#8211; \u9519\u8bef\u7387: &lt; 1%<br \/>\n  &#8211; \u7f13\u5b58\u547d\u4e2d\u7387: &gt; 60%<br \/>\n  &#8211; \u79bb\u7ebf\u529f\u80fd\u53ef\u7528\u6027: 100%<\/p>\n<p>\u72b6\u6001\u7801\u5206\u5e03\u76d1\u63a7:<br \/>\n  &#8211; 2xx: &gt; 95%<br \/>\n  &#8211; 4xx: &lt; 3%<br \/>\n  &#8211; 5xx: &lt; 2%<\/p>\n<p>\u7528\u6237\u4f53\u9a8c\u6307\u6807:<br \/>\n  &#8211; \u9996\u6b21\u52a0\u8f7d\u65f6\u95f4: &lt; 3\u79d2<br \/>\n  &#8211; \u4ea4\u4e92\u54cd\u5e94\u65f6\u95f4: &lt; 100\u6beb\u79d2<br \/>\n  &#8211; \u79bb\u7ebf\u64cd\u4f5c\u6210\u529f\u7387: &gt; 90%<\/p>\n<h3>32.10 \u672a\u6765\u8d8b\u52bf<\/h3>\n<h4>32.10.1 5G\u7f51\u7edc\u7684\u5f71\u54cd<\/h4>\n<p>typescript<\/p>\n<p>\/\/ 5G\u7f51\u7edc\u611f\u77e5\u7684\u4f18\u5316\u7b56\u7565<br \/>\nclass FiveGNetworkOptimizer {<br \/>\n  private networkInfo: NetworkInformation;<\/p>\n<p>  constructor() {<br \/>\n    if (&#039;connection&#039; in navigator) {<br \/>\n      this.networkInfo &#061; (navigator as any).connection;<br \/>\n      this.setupNetworkMonitoring();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  private setupNetworkMonitoring() {<br \/>\n    this.networkInfo.addEventListener(&#039;change&#039;, () &#061;&gt; {<br \/>\n      this.adaptToNetworkType();<br \/>\n    });<br \/>\n  }<\/p>\n<p>  private adaptToNetworkType() {<br \/>\n    const { effectiveType, downlink, rtt } &#061; this.networkInfo;<\/p>\n<p>    if (effectiveType &#061;&#061;&#061; &#039;5g&#039; &amp;&amp; downlink &gt; 100) {<br \/>\n      \/\/ 5G\u9ad8\u901f\u7f51\u7edc<br \/>\n      this.enableHighQualityStreaming();<br \/>\n      this.prefetchMoreContent();<br \/>\n      this.useLessCompression();<br \/>\n    } else if (effectiveType &#061;&#061;&#061; &#039;4g&#039;) {<br \/>\n      \/\/ 4G\u7f51\u7edc<br \/>\n      this.useBalancedStrategy();<br \/>\n    } else {<br \/>\n      \/\/ \u6162\u901f\u7f51\u7edc<br \/>\n      this.useConservativeStrategy();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  private enableHighQualityStreaming() {<br \/>\n    \/\/ \u542f\u7528\u9ad8\u6e05\u89c6\u9891<br \/>\n    \/\/ \u63d0\u9ad8\u56fe\u7247\u8d28\u91cf<br \/>\n    \/\/ \u51cf\u5c11\u538b\u7f29\u7387<br \/>\n  }<\/p>\n<p>  private prefetchMoreContent() {<br \/>\n    \/\/ \u9884\u52a0\u8f7d\u66f4\u591a\u5185\u5bb9<br \/>\n    \/\/ \u63d0\u524d\u83b7\u53d6\u4e0b\u4e00\u9875\u6570\u636e<br \/>\n    \/\/ \u7f13\u5b58\u76f8\u5173\u8d44\u6e90<br \/>\n  }<\/p>\n<p>  private useLessCompression() {<br \/>\n    \/\/ \u4f7f\u7528\u539f\u59cb\u8d28\u91cf\u56fe\u7247<br \/>\n    \/\/ \u51cf\u5c11\u6570\u636e\u538b\u7f29<br \/>\n    \/\/ \u4f7f\u7528\u66f4\u9ad8\u6548\u4f46\u4e0d\u4e00\u5b9a\u9ad8\u538b\u7f29\u7684\u683c\u5f0f<br \/>\n  }<br \/>\n}<\/p>\n<h4>32.10.2 \u8fb9\u7f18\u8ba1\u7b97\u4e0e\u79fb\u52a8\u7aef<\/h4>\n<p>\u968f\u7740\u8fb9\u7f18\u8ba1\u7b97\u7684\u53d1\u5c55&#xff0c;\u79fb\u52a8\u5e94\u7528\u53ef\u4ee5&#xff1a;<\/p>\n<li>\n<p>\u66f4\u4f4e\u7684\u5ef6\u8fdf&#xff1a;\u901a\u8fc7\u8fb9\u7f18\u8282\u70b9\u51cf\u5c11\u7f51\u7edc\u5ef6\u8fdf<\/p>\n<\/li>\n<li>\n<p>\u66f4\u597d\u7684\u79bb\u7ebf\u4f53\u9a8c&#xff1a;\u8fb9\u7f18\u7f13\u5b58\u63d0\u4f9b\u66f4\u4e30\u5bcc\u7684\u79bb\u7ebf\u5185\u5bb9<\/p>\n<\/li>\n<li>\n<p>\u667a\u80fd\u8def\u7531&#xff1a;\u6839\u636e\u7528\u6237\u4f4d\u7f6e\u9009\u62e9\u6700\u4f18\u670d\u52a1\u8282\u70b9<\/p>\n<\/li>\n<li>\n<p>\u5b9e\u65f6\u540c\u6b65&#xff1a;\u8fb9\u7f18\u8282\u70b9\u95f4\u7684\u6570\u636e\u540c\u6b65\u66f4\u5feb\u901f<\/p>\n<\/li>\n<p>typescript<\/p>\n<p>\/\/ \u8fb9\u7f18\u8ba1\u7b97\u611f\u77e5\u7684\u8bf7\u6c42\u8def\u7531<br \/>\nclass EdgeAwareHttpClient extends RNHttpClient {<br \/>\n  private edgeNodes: EdgeNode[];<br \/>\n  private userLocation: Location | null;<\/p>\n<p>  async getOptimalEndpoint(service: string): Promise&lt;string&gt; {<br \/>\n    if (!this.userLocation) {<br \/>\n      return &#096;${this.baseURL}\/${service}&#096;;<br \/>\n    }<\/p>\n<p>    \/\/ \u627e\u5230\u6700\u8fd1\u7684\u8fb9\u7f18\u8282\u70b9<br \/>\n    const nearestNode &#061; this.findNearestEdgeNode(this.userLocation);<\/p>\n<p>    if (nearestNode) {<br \/>\n      \/\/ \u68c0\u67e5\u8fb9\u7f18\u8282\u70b9\u5065\u5eb7\u72b6\u6001<br \/>\n      const isHealthy &#061; await this.checkNodeHealth(nearestNode);<\/p>\n<p>      if (isHealthy) {<br \/>\n        return &#096;${nearestNode.url}\/${service}&#096;;<br \/>\n      }<br \/>\n    }<\/p>\n<p>    \/\/ \u56de\u9000\u5230\u4e3b\u8282\u70b9<br \/>\n    return &#096;${this.baseURL}\/${service}&#096;;<br \/>\n  }<\/p>\n<p>  private findNearestEdgeNode(location: Location): EdgeNode | null {<br \/>\n    let nearest: EdgeNode | null &#061; null;<br \/>\n    let minDistance &#061; Infinity;<\/p>\n<p>    for (const node of this.edgeNodes) {<br \/>\n      const distance &#061; this.calculateDistance(location, node.location);<br \/>\n      if (distance &lt; minDistance) {<br \/>\n        minDistance &#061; distance;<br \/>\n        nearest &#061; node;<br \/>\n      }<br \/>\n    }<\/p>\n<p>    return nearest;<br \/>\n  }<\/p>\n<p>  private async checkNodeHealth(node: EdgeNode): Promise&lt;boolean&gt; {<br \/>\n    try {<br \/>\n      const response &#061; await fetch(&#096;${node.url}\/health&#096;, {<br \/>\n        timeout: 5000,<br \/>\n      });<\/p>\n<p>      return response.ok &amp;&amp; response.status &#061;&#061;&#061; 200;<br \/>\n    } catch {<br \/>\n      return false;<br \/>\n    }<br \/>\n  }<\/p>\n<p>  async request&lt;T&gt;(<br \/>\n    endpoint: string,<br \/>\n    options: RequestOptions &#061; {}<br \/>\n  ): Promise&lt;ApiResponse&lt;T&gt;&gt; {<br \/>\n    \/\/ \u83b7\u53d6\u6700\u4f18\u7aef\u70b9<br \/>\n    const optimalEndpoint &#061; await this.getOptimalEndpoint(endpoint);<\/p>\n<p>    \/\/ \u4f7f\u7528\u7236\u7c7b\u65b9\u6cd5\u53d1\u9001\u8bf7\u6c42<br \/>\n    return super.request(optimalEndpoint, options);<br \/>\n  }<br \/>\n}<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u7b2c30\u7ae0&#xff1a;RESTful API\u8bbe\u8ba1\u4e2d\u7684\u72b6\u6001\u7801\u4f7f\u752830.1 RESTful\u67b6\u6784\u4e0e\u72b6\u6001\u7801\u7684\u54f2\u5b66RESTful API\u8bbe\u8ba1\u4e2d&#xff0c;HTTP\u72b6\u6001\u7801\u4e0d\u4ec5\u662f\u6280\u672f\u5b9e\u73b0\u7ec6\u8282&#xff0c;\u66f4\u662fAPI\u5951\u7ea6\u7684\u91cd\u8981\u7ec4\u6210\u90e8\u5206\u3002\u5b83\u4eec\u6784\u6210\u4e86\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u4e4b\u95f4\u901a\u4fe1\u7684\u8bed\u4e49\u6846\u67b6&#xff0c;\u4f7f\u5f97API\u80fd\u591f\u81ea\u6211\u63cf\u8ff0\u5176\u5904\u7406\u7ed3\u679c\u300230.1.1 Richardson\u6210\u719f\u5ea6\u6a21\u578b\u4e0e\u72b6\u6001\u7801Richardson\u6210\u719f\u5ea6\u6a21\u578b\u5c06RESTful\u670d\u52a1\u5206\u4e3a\u56db\u4e2a\u7b49\u7ea7&#xff1a;Level 0&#xff1a;<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[151,43,191],"topic":[],"class_list":["post-86703","post","type-post","status-publish","format-standard","hentry","category-server","tag-http","tag-43","tag-191"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v20.3 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>HTTP \u72b6\u6001\u7801\uff1a\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u901a\u4fe1\u8bed\u8a00\u2014\u2014\u7b2c\u516d\u90e8\u5206\uff1a\u72b6\u6001\u7801\u7684\u5b9e\u8df5\u5e94\u7528\uff08\u4e00\uff09 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.wsisp.com\/helps\/86703.html\" \/>\n<meta property=\"og:locale\" content=\"zh_CN\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"HTTP \u72b6\u6001\u7801\uff1a\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u901a\u4fe1\u8bed\u8a00\u2014\u2014\u7b2c\u516d\u90e8\u5206\uff1a\u72b6\u6001\u7801\u7684\u5b9e\u8df5\u5e94\u7528\uff08\u4e00\uff09 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3\" \/>\n<meta property=\"og:description\" content=\"\u7b2c30\u7ae0&#xff1a;RESTful API\u8bbe\u8ba1\u4e2d\u7684\u72b6\u6001\u7801\u4f7f\u752830.1 RESTful\u67b6\u6784\u4e0e\u72b6\u6001\u7801\u7684\u54f2\u5b66RESTful API\u8bbe\u8ba1\u4e2d&#xff0c;HTTP\u72b6\u6001\u7801\u4e0d\u4ec5\u662f\u6280\u672f\u5b9e\u73b0\u7ec6\u8282&#xff0c;\u66f4\u662fAPI\u5951\u7ea6\u7684\u91cd\u8981\u7ec4\u6210\u90e8\u5206\u3002\u5b83\u4eec\u6784\u6210\u4e86\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u4e4b\u95f4\u901a\u4fe1\u7684\u8bed\u4e49\u6846\u67b6&#xff0c;\u4f7f\u5f97API\u80fd\u591f\u81ea\u6211\u63cf\u8ff0\u5176\u5904\u7406\u7ed3\u679c\u300230.1.1 Richardson\u6210\u719f\u5ea6\u6a21\u578b\u4e0e\u72b6\u6001\u7801Richardson\u6210\u719f\u5ea6\u6a21\u578b\u5c06RESTful\u670d\u52a1\u5206\u4e3a\u56db\u4e2a\u7b49\u7ea7&#xff1a;Level 0&#xff1a;\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.wsisp.com\/helps\/86703.html\" \/>\n<meta property=\"og:site_name\" content=\"\u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3\" \/>\n<meta property=\"article:published_time\" content=\"2026-07-28T20:32:41+00:00\" \/>\n<meta name=\"author\" content=\"admin\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u4f5c\u8005\" \/>\n\t<meta name=\"twitter:data1\" content=\"admin\" \/>\n\t<meta name=\"twitter:label2\" content=\"\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4\" \/>\n\t<meta name=\"twitter:data2\" content=\"47 \u5206\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/86703.html\",\"url\":\"https:\/\/www.wsisp.com\/helps\/86703.html\",\"name\":\"HTTP \u72b6\u6001\u7801\uff1a\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u901a\u4fe1\u8bed\u8a00\u2014\u2014\u7b2c\u516d\u90e8\u5206\uff1a\u72b6\u6001\u7801\u7684\u5b9e\u8df5\u5e94\u7528\uff08\u4e00\uff09 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3\",\"isPartOf\":{\"@id\":\"https:\/\/www.wsisp.com\/helps\/#website\"},\"datePublished\":\"2026-07-28T20:32:41+00:00\",\"dateModified\":\"2026-07-28T20:32:41+00:00\",\"author\":{\"@id\":\"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/358e386c577a3ab51c4493330a20ad41\"},\"breadcrumb\":{\"@id\":\"https:\/\/www.wsisp.com\/helps\/86703.html#breadcrumb\"},\"inLanguage\":\"zh-Hans\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.wsisp.com\/helps\/86703.html\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/86703.html#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"\u9996\u9875\",\"item\":\"https:\/\/www.wsisp.com\/helps\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"HTTP \u72b6\u6001\u7801\uff1a\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u901a\u4fe1\u8bed\u8a00\u2014\u2014\u7b2c\u516d\u90e8\u5206\uff1a\u72b6\u6001\u7801\u7684\u5b9e\u8df5\u5e94\u7528\uff08\u4e00\uff09\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/#website\",\"url\":\"https:\/\/www.wsisp.com\/helps\/\",\"name\":\"\u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3\",\"description\":\"\u9999\u6e2f\u670d\u52a1\u5668_\u9999\u6e2f\u4e91\u670d\u52a1\u5668\u8d44\u8baf_\u670d\u52a1\u5668\u5e2e\u52a9\u6587\u6863_\u670d\u52a1\u5668\u6559\u7a0b\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.wsisp.com\/helps\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"zh-Hans\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/358e386c577a3ab51c4493330a20ad41\",\"name\":\"admin\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"zh-Hans\",\"@id\":\"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/gravatar.wp-china-yes.net\/avatar\/?s=96&d=mystery\",\"contentUrl\":\"https:\/\/gravatar.wp-china-yes.net\/avatar\/?s=96&d=mystery\",\"caption\":\"admin\"},\"sameAs\":[\"http:\/\/wp.wsisp.com\"],\"url\":\"https:\/\/www.wsisp.com\/helps\/author\/admin\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"HTTP \u72b6\u6001\u7801\uff1a\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u901a\u4fe1\u8bed\u8a00\u2014\u2014\u7b2c\u516d\u90e8\u5206\uff1a\u72b6\u6001\u7801\u7684\u5b9e\u8df5\u5e94\u7528\uff08\u4e00\uff09 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.wsisp.com\/helps\/86703.html","og_locale":"zh_CN","og_type":"article","og_title":"HTTP \u72b6\u6001\u7801\uff1a\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u901a\u4fe1\u8bed\u8a00\u2014\u2014\u7b2c\u516d\u90e8\u5206\uff1a\u72b6\u6001\u7801\u7684\u5b9e\u8df5\u5e94\u7528\uff08\u4e00\uff09 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","og_description":"\u7b2c30\u7ae0&#xff1a;RESTful API\u8bbe\u8ba1\u4e2d\u7684\u72b6\u6001\u7801\u4f7f\u752830.1 RESTful\u67b6\u6784\u4e0e\u72b6\u6001\u7801\u7684\u54f2\u5b66RESTful API\u8bbe\u8ba1\u4e2d&#xff0c;HTTP\u72b6\u6001\u7801\u4e0d\u4ec5\u662f\u6280\u672f\u5b9e\u73b0\u7ec6\u8282&#xff0c;\u66f4\u662fAPI\u5951\u7ea6\u7684\u91cd\u8981\u7ec4\u6210\u90e8\u5206\u3002\u5b83\u4eec\u6784\u6210\u4e86\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u4e4b\u95f4\u901a\u4fe1\u7684\u8bed\u4e49\u6846\u67b6&#xff0c;\u4f7f\u5f97API\u80fd\u591f\u81ea\u6211\u63cf\u8ff0\u5176\u5904\u7406\u7ed3\u679c\u300230.1.1 Richardson\u6210\u719f\u5ea6\u6a21\u578b\u4e0e\u72b6\u6001\u7801Richardson\u6210\u719f\u5ea6\u6a21\u578b\u5c06RESTful\u670d\u52a1\u5206\u4e3a\u56db\u4e2a\u7b49\u7ea7&#xff1a;Level 0&#xff1a;","og_url":"https:\/\/www.wsisp.com\/helps\/86703.html","og_site_name":"\u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","article_published_time":"2026-07-28T20:32:41+00:00","author":"admin","twitter_card":"summary_large_image","twitter_misc":{"\u4f5c\u8005":"admin","\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4":"47 \u5206"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.wsisp.com\/helps\/86703.html","url":"https:\/\/www.wsisp.com\/helps\/86703.html","name":"HTTP \u72b6\u6001\u7801\uff1a\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u901a\u4fe1\u8bed\u8a00\u2014\u2014\u7b2c\u516d\u90e8\u5206\uff1a\u72b6\u6001\u7801\u7684\u5b9e\u8df5\u5e94\u7528\uff08\u4e00\uff09 - \u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","isPartOf":{"@id":"https:\/\/www.wsisp.com\/helps\/#website"},"datePublished":"2026-07-28T20:32:41+00:00","dateModified":"2026-07-28T20:32:41+00:00","author":{"@id":"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/358e386c577a3ab51c4493330a20ad41"},"breadcrumb":{"@id":"https:\/\/www.wsisp.com\/helps\/86703.html#breadcrumb"},"inLanguage":"zh-Hans","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.wsisp.com\/helps\/86703.html"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.wsisp.com\/helps\/86703.html#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"\u9996\u9875","item":"https:\/\/www.wsisp.com\/helps"},{"@type":"ListItem","position":2,"name":"HTTP \u72b6\u6001\u7801\uff1a\u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u901a\u4fe1\u8bed\u8a00\u2014\u2014\u7b2c\u516d\u90e8\u5206\uff1a\u72b6\u6001\u7801\u7684\u5b9e\u8df5\u5e94\u7528\uff08\u4e00\uff09"}]},{"@type":"WebSite","@id":"https:\/\/www.wsisp.com\/helps\/#website","url":"https:\/\/www.wsisp.com\/helps\/","name":"\u7f51\u7855\u4e92\u8054\u5e2e\u52a9\u4e2d\u5fc3","description":"\u9999\u6e2f\u670d\u52a1\u5668_\u9999\u6e2f\u4e91\u670d\u52a1\u5668\u8d44\u8baf_\u670d\u52a1\u5668\u5e2e\u52a9\u6587\u6863_\u670d\u52a1\u5668\u6559\u7a0b","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.wsisp.com\/helps\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"zh-Hans"},{"@type":"Person","@id":"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/358e386c577a3ab51c4493330a20ad41","name":"admin","image":{"@type":"ImageObject","inLanguage":"zh-Hans","@id":"https:\/\/www.wsisp.com\/helps\/#\/schema\/person\/image\/","url":"https:\/\/gravatar.wp-china-yes.net\/avatar\/?s=96&d=mystery","contentUrl":"https:\/\/gravatar.wp-china-yes.net\/avatar\/?s=96&d=mystery","caption":"admin"},"sameAs":["http:\/\/wp.wsisp.com"],"url":"https:\/\/www.wsisp.com\/helps\/author\/admin"}]}},"_links":{"self":[{"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/posts\/86703","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/comments?post=86703"}],"version-history":[{"count":0,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/posts\/86703\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/media?parent=86703"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/categories?post=86703"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/tags?post=86703"},{"taxonomy":"topic","embeddable":true,"href":"https:\/\/www.wsisp.com\/helps\/wp-json\/wp\/v2\/topic?post=86703"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}