從網際網路擷取資料
大多數應用程式都需要某種形式的通訊或從網際網路擷取資料。許多應用程式透過 HTTP 請求來實現,這些請求從用戶端發送到伺服器,以針對透過 URI (Uniform Resource Identifier) 識別的資源執行特定操作。
透過 HTTP 傳輸的資料在技術上可以是任何形式,但使用 JSON (JavaScript Object Notation) 是一種常見的選擇,因為它具有人類可讀性且與語言無關。 Dart SDK 和生態系統也對 JSON 提供廣泛的支援,並提供多種選項以最佳地滿足您應用程式的需求。
在本教學中,您將深入瞭解 HTTP 請求、URI 和 JSON。然後,您將學習如何使用 package:http
以及 Dart 在 dart:convert
程式庫中對 JSON 的支援,以擷取、解碼,然後使用從 HTTP 伺服器擷取的 JSON 格式資料。
背景概念
#以下章節提供關於本教學中使用的技術和概念的一些額外背景資訊,以方便從伺服器擷取資料。若要直接跳到教學內容,請參閱〈擷取必要的相依性〉。
JSON
#JSON (JavaScript Object Notation) 是一種資料交換格式,已在應用程式開發和用戶端-伺服器通訊中變得無處不在。它很輕量級,但由於是基於文字的,因此也易於人類讀寫。透過 JSON,可以序列化各種資料類型和簡單的資料結構,例如列表和映射,並以字串表示。
大多數語言都有許多實作,並且解析器已變得非常快速,因此您無需擔心互通性或效能。有關 JSON 格式的更多資訊,請參閱〈Introducing JSON〉。若要深入瞭解如何在 Dart 中使用 JSON,請參閱〈Using JSON〉指南。
HTTP 請求
#HTTP (Hypertext Transfer Protocol) 是一種無狀態協定,旨在傳輸文件,最初是在 Web 用戶端和 Web 伺服器之間。您與此協定互動以載入此頁面,因為您的瀏覽器使用 HTTP GET
請求從 Web 伺服器擷取頁面內容。自推出以來,HTTP 協定及其各種版本的用途已擴展到 Web 以外的應用程式,基本上是任何需要用戶端到伺服器通訊的地方。
從用戶端發送到伺服器以進行通訊的 HTTP 請求由多個組件組成。 HTTP 程式庫 (例如 package:http
) 允許您指定以下類型的通訊
- 定義所需動作的 HTTP 方法,例如
GET
用於擷取資料,或POST
用於提交新資料。 - 資源的位置,透過 URI。
- 正在使用的 HTTP 版本。
- 提供額外資訊給伺服器的標頭。
- 一個可選的本文,以便請求可以將資料發送到伺服器,而不僅僅是擷取資料。
若要深入瞭解 HTTP 協定,請查看 mdn web docs 上的〈An overview of HTTP〉。
URIs 與 URLs
#若要發出 HTTP 請求,您需要提供資源的 URI (Uniform Resource Identifier)。 URI 是一個唯一識別資源的字串。 URL (Uniform Resource Locator) 是一種特定類型的 URI,它也提供資源的位置。 Web 上資源的 URL 包含三個資訊片段。對於目前頁面,URL 由以下部分組成
- 用於確定所用協定的方案:
https
- 伺服器的授權或主機名稱:
dart.dev
- 資源的路徑:
/tutorials/server/fetch-data.html
還有其他可選參數,目前頁面未使用
- 用於自訂額外行為的參數:
?key1=value1&key2=value2
- 錨點,不會發送到伺服器,指向資源中的特定位置:
#uris
若要深入瞭解 URL,請參閱 mdn web docs 上的〈What is a URL?〉。
擷取必要的相依性
#package:http
程式庫提供跨平台解決方案,用於發出可組合的 HTTP 請求,並具有可選的細粒度控制。
若要新增對 package:http
的相依性,請從您的 repo 頂層執行以下 dart pub add
命令
$ dart pub add http
若要在您的程式碼中使用 package:http
,請匯入它,並可選擇指定程式庫前綴
import 'package:http/http.dart' as http;
若要深入瞭解 package:http
的具體資訊,請參閱其在 pub.dev 網站上的頁面及其 API 文件。
建立 URL
#如先前所述,若要發出 HTTP 請求,您首先需要一個 URL,用於識別所請求的資源或正在存取的端點。
在 Dart 中,URL 透過 Uri
物件表示。有很多方法可以建立 Uri
,但由於其靈活性,使用 Uri.parse
解析字串來建立一個是很常見的解決方案。
以下程式碼片段顯示了兩種建立 Uri
物件的方法,該物件指向託管在本網站上的關於 package:http
的模擬 JSON 格式資訊
// Parse the entire URI, including the scheme
Uri.parse('https://dart.dev.org.tw/f/packages/http.json');
// Specifically create a URI with the https scheme
Uri.https('dart.dev', '/f/packages/http.json');
若要瞭解其他建立和與 URI 互動的方式,請參閱 URI
文件。
發出網路請求
#如果您只需要快速擷取所請求資源的字串表示形式,可以使用在 package:http
中找到的頂層 read
函式,如果請求不成功,它會傳回 Future<String>
或拋出 ClientException
。以下範例使用 read
擷取關於 package:http
的模擬 JSON 格式資訊作為字串,然後將其列印出來
void main() async {
final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
final httpPackageInfo = await http.read(httpPackageUrl);
print(httpPackageInfo);
}
這會產生以下 JSON 格式的輸出,您也可以在瀏覽器中透過 /f/packages/http.json
查看。
{
"name": "http",
"latestVersion": "1.1.2",
"description": "A composable, multi-platform, Future-based API for HTTP requests.",
"publisher": "dart.dev",
"repository": "https://github.com/dart-lang/http"
}
請注意資料的結構 (在本例中為映射),因為稍後在解碼 JSON 時您將需要它。
如果您需要來自回應的其他資訊,例如狀態碼或標頭,您可以改用頂層 get
函式,它會傳回帶有 Response
的 Future
。
以下程式碼片段使用 get
取得完整回應,以便在請求不成功時提前退出,這以狀態碼 200 表示
void main() async {
final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
final httpPackageResponse = await http.get(httpPackageUrl);
if (httpPackageResponse.statusCode != 200) {
print('Failed to retrieve the http package!');
return;
}
print(httpPackageResponse.body);
}
除了 200 之外,還有許多其他狀態碼,您的應用程式可能希望以不同的方式處理它們。若要深入瞭解不同狀態碼的含義,請參閱 mdn web docs 上的〈HTTP response status codes〉。
某些伺服器請求需要更多資訊,例如身份驗證或使用者代理程式資訊;在這種情況下,您可能需要包含 HTTP 標頭。您可以透過傳入鍵值對的 Map<String, String>
作為 headers
可選的命名參數來指定標頭
await http.get(
Uri.https('dart.dev', '/f/packages/http.json'),
headers: {'User-Agent': '<product name>/<product-version>'},
);
發出多個請求
#如果您要對同一伺服器發出多個請求,您可以改為透過 Client
保留持久連線,它具有與頂層方法類似的方法。只需確保在完成後使用 close
方法清理。
void main() async {
final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
final client = http.Client();
try {
final httpPackageInfo = await client.read(httpPackageUrl);
print(httpPackageInfo);
} finally {
client.close();
}
}
若要使用戶端能夠重試失敗的請求,請匯入 package:http/retry.dart
並將您建立的 Client
包裝在 RetryClient
中
import 'package:http/http.dart' as http;
import 'package:http/retry.dart';
void main() async {
final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
final client = RetryClient(http.Client());
try {
final httpPackageInfo = await client.read(httpPackageUrl);
print(httpPackageInfo);
} finally {
client.close();
}
}
RetryClient
對於重試次數以及每次請求之間等待的時間具有預設行為,但其行為可以透過 RetryClient()
或 RetryClient.withDelays()
建構子的參數進行修改。
package:http
具有更多功能和自訂選項,因此請務必查看其在 pub.dev 網站上的頁面及其 API 文件。
解碼擷取的資料
#雖然您現在已發出網路請求並以字串形式擷取了傳回的資料,但從字串中存取特定部分的資訊可能是一項挑戰。
由於資料已採用 JSON 格式,因此您可以使用 Dart 內建的 json.decode
函式 (位於 dart:convert
程式庫中) 將原始字串轉換為使用 Dart 物件的 JSON 表示形式。在本例中,JSON 資料以映射結構表示,並且在 JSON 中,映射鍵始終是字串,因此您可以將 json.decode
的結果轉換為 Map<String, dynamic>
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() async {
final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
final httpPackageInfo = await http.read(httpPackageUrl);
final httpPackageJson = json.decode(httpPackageInfo) as Map<String, dynamic>;
print(httpPackageJson);
}
建立結構化類別來儲存資料
#為了為解碼後的 JSON 提供更多結構,使其更易於使用,請建立一個類別,該類別可以使用特定類型 (取決於您的資料的結構描述) 來儲存擷取的資料。
以下程式碼片段顯示了基於類別的表示形式,該形式可以儲存從您請求的模擬 JSON 檔案傳回的套件資訊。此結構假設除了 repository 之外的所有欄位都是必需的,並且每次都提供。
class PackageInfo {
final String name;
final String latestVersion;
final String description;
final String publisher;
final Uri? repository;
PackageInfo({
required this.name,
required this.latestVersion,
required this.description,
required this.publisher,
this.repository,
});
}
將資料轉換為您的類別
#現在您有了一個類別來儲存您的資料,您需要新增一種機制將解碼後的 JSON 轉換為 PackageInfo
物件。
透過手動編寫與先前的 JSON 格式匹配的 fromJson
方法來轉換解碼後的 JSON,根據需要轉換類型並處理可選的 repository 欄位
class PackageInfo {
// ···
factory PackageInfo.fromJson(Map<String, dynamic> json) {
final repository = json['repository'] as String?;
return PackageInfo(
name: json['name'] as String,
latestVersion: json['latestVersion'] as String,
description: json['description'] as String,
publisher: json['publisher'] as String,
repository: repository != null ? Uri.tryParse(repository) : null,
);
}
}
手寫方法 (例如先前的範例) 通常足以應對相對簡單的 JSON 結構,但也有更靈活的選項。若要深入瞭解 JSON 序列化和反序列化,包括自動產生轉換邏輯,請參閱〈Using JSON〉指南。
將回應轉換為結構化類別的物件
#現在您有一個類別來儲存您的資料,並且有一種方法將解碼後的 JSON 物件轉換為該類型的物件。接下來,您可以編寫一個將所有內容整合在一起的函式
- 根據傳入的套件名稱建立您的
URI
。 - 使用
http.get
擷取該套件的資料。 - 如果請求未成功,則拋出
Exception
或最好是您自己的自訂Exception
子類別。 - 如果請求成功,則使用
json.decode
將回應本文解碼為 JSON 字串。 - 使用您建立的
PackageInfo.fromJson
工廠建構子將解碼後的 JSON 字串轉換為PackageInfo
物件。
Future<PackageInfo> getPackage(String packageName) async {
final packageUrl = Uri.https('dart.dev', '/f/packages/$packageName.json');
final packageResponse = await http.get(packageUrl);
// If the request didn't succeed, throw an exception
if (packageResponse.statusCode != 200) {
throw PackageRetrievalException(
packageName: packageName,
statusCode: packageResponse.statusCode,
);
}
final packageJson = json.decode(packageResponse.body) as Map<String, dynamic>;
return PackageInfo.fromJson(packageJson);
}
class PackageRetrievalException implements Exception {
final String packageName;
final int? statusCode;
PackageRetrievalException({required this.packageName, this.statusCode});
}
運用轉換後的資料
#現在您已擷取資料並將其轉換為更易於存取的格式,您可以隨意使用它。一些可能性包括將資訊輸出到 CLI,或在 Web 或 Flutter 應用程式中顯示它。
這是一個完整的、可執行的範例,它請求、解碼,然後顯示關於 http
和 path
套件的模擬資訊
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() async {
await printPackageInformation('http');
print('');
await printPackageInformation('path');
}
Future<void> printPackageInformation(String packageName) async {
final PackageInfo packageInfo;
try {
packageInfo = await getPackage(packageName);
} on PackageRetrievalException catch (e) {
print(e);
return;
}
print('Information about the $packageName package:');
print('Latest version: ${packageInfo.latestVersion}');
print('Description: ${packageInfo.description}');
print('Publisher: ${packageInfo.publisher}');
final repository = packageInfo.repository;
if (repository != null) {
print('Repository: $repository');
}
}
Future<PackageInfo> getPackage(String packageName) async {
final packageUrl = Uri.https('dart.dev', '/f/packages/$packageName.json');
final packageResponse = await http.get(packageUrl);
// If the request didn't succeed, throw an exception
if (packageResponse.statusCode != 200) {
throw PackageRetrievalException(
packageName: packageName,
statusCode: packageResponse.statusCode,
);
}
final packageJson = json.decode(packageResponse.body) as Map<String, dynamic>;
return PackageInfo.fromJson(packageJson);
}
class PackageInfo {
final String name;
final String latestVersion;
final String description;
final String publisher;
final Uri? repository;
PackageInfo({
required this.name,
required this.latestVersion,
required this.description,
required this.publisher,
this.repository,
});
factory PackageInfo.fromJson(Map<String, dynamic> json) {
final repository = json['repository'] as String?;
return PackageInfo(
name: json['name'] as String,
latestVersion: json['latestVersion'] as String,
description: json['description'] as String,
publisher: json['publisher'] as String,
repository: repository != null ? Uri.tryParse(repository) : null,
);
}
}
class PackageRetrievalException implements Exception {
final String packageName;
final int? statusCode;
PackageRetrievalException({required this.packageName, this.statusCode});
@override
String toString() {
final buf = StringBuffer();
buf.write('Failed to retrieve package:$packageName information');
if (statusCode != null) {
buf.write(' with a status code of $statusCode');
}
buf.write('!');
return buf.toString();
}
}
下一步?
#現在您已從網際網路擷取、解析和使用資料,請考慮深入瞭解〈Concurrency in Dart〉。如果您的資料很大且很複雜,您可以將擷取和解碼移至另一個 isolate 作為背景工作程式,以防止您的介面變得無回應。
除非另有說明,否則本網站上的文件反映 Dart 3.7.1 版本。頁面最近更新於 2025-02-12。 檢視原始碼 或 回報問題。