從網際網路獲取資料
大多數應用程式需要某種形式的通訊或從網際網路獲取資料。許多應用程式透過 HTTP 請求來完成此操作,這些請求從客戶端傳送到伺服器,以對透過 URI (統一資源識別符號) 標識的資源執行特定操作。
透過 HTTP 傳輸的資料技術上可以是任何形式,但由於其易讀性和語言無關性,使用 JSON (JavaScript 物件表示法) 是一個流行的選擇。Dart SDK 和生態系統也對 JSON 提供了廣泛的支援,並提供了多種選項以最好地滿足你的應用程式需求。
在本教程中,你將學習更多關於 HTTP 請求、URI 和 JSON 的知識。然後,你將學習如何使用 package:http 以及 Dart 在 dart:convert 庫中對 JSON 的支援來獲取、解碼,然後使用從 HTTP 伺服器獲取的 JSON 格式資料。
背景概念
#以下部分提供了關於教程中使用的技術和概念的一些額外背景資訊,以便於從伺服器獲取資料。要直接跳到教程內容,請參閱獲取必要的依賴項。
JSON
#JSON (JavaScript 物件表示法) 是一種資料交換格式,已在應用程式開發和客戶端-伺服器通訊中變得無處不在。它輕巧且基於文字,因此也易於人類讀寫。透過 JSON,各種資料型別和簡單的 資料結構(如列表和對映)可以被序列化並表示為字串。
大多數語言都有許多實現,並且解析器變得非常快,因此你無需擔心互操作性或效能。有關 JSON 格式的更多資訊,請參閱JSON 簡介。要了解更多關於在 Dart 中使用 JSON 的資訊,請參閱使用 JSON 指南。
HTTP 請求
#HTTP (超文字傳輸協議) 是一種無狀態協議,設計用於傳輸文件,最初用於 Web 客戶端和 Web 伺服器之間。你與該協議進行了互動以載入此頁面,因為你的瀏覽器使用 HTTP GET 請求從 Web 伺服器獲取頁面內容。自引入以來,HTTP 協議及其各種版本的使用也擴充套件到了 Web 之外的應用,基本上只要需要客戶端與伺服器通訊的地方都可以使用。
從客戶端傳送到伺服器的 HTTP 請求由多個組成部分構成。HTTP 庫,例如 package:http,允許你指定以下型別的通訊
- 定義所需操作的 HTTP 方法,例如
GET用於獲取資料,或POST用於提交新資料。 - 透過 URI 指定資源的 位置。
- 正在使用的 HTTP 版本。
- 向伺服器提供額外資訊的 Header(頭部)欄位。
- 一個可選的 Body(主體),以便請求可以向伺服器傳送資料,而不僅僅是檢索資料。
要了解更多關於 HTTP 協議的資訊,請查閱 mdn web docs 上的HTTP 概述。
URI 與 URL
#要發起 HTTP 請求,你需要提供一個指向資源的 URI (統一資源識別符號)。URI 是唯一標識資源的字串。URL (統一資源定位符) 是一種特殊的 URI,它同時提供了資源的位置。Web 資源的 URL 包含三個資訊片段。對於當前頁面,URL 由以下部分組成:
- 用於確定所使用協議的方案 (scheme):
https - 伺服器的權威機構或主機名 (authority or hostname):
dart.dev - 資源的路徑 (path):
/tutorials/server/fetch-data.html
還有一些當前頁面未使用的可選引數
- 用於自定義額外行為的引數 (parameters):
?key1=value1&key2=value2 - 一個錨點 (anchor),不會發送到伺服器,指向資源中的特定位置:
#uris
要了解更多關於 URL 的資訊,請查閱 mdn web docs 上的什麼是 URL?。
獲取必要的依賴項
#package:http 庫提供了一個跨平臺的解決方案,用於發起可組合的 HTTP 請求,並提供可選的細粒度控制。
要新增對 package:http 的依賴,請在你的倉庫根目錄執行以下 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 格式資訊的 Uri 物件的方法
// Parse the entire URI, including the scheme
Uri.parse('https://dart.lang.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"
}請注意資料的結構(在本例中是一個 map),稍後在解碼 JSON 時會用到它。
如果需要從響應中獲取其他資訊,例如狀態碼或Header(頭部),可以使用頂級函式 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 響應狀態碼。
有些伺服器請求需要更多資訊,例如身份驗證或使用者代理資訊;在這種情況下,你可能需要包含HTTP Header(頭部)。可以透過將鍵值對的 Map<String, String> 作為可選的命名引數 headers 傳入來指定 Header。
await http.get(
Uri.https('dart.dev', '/f/packages/http.json'),
headers: {'User-Agent': '<product name>/<product-version>'},
);發起多個請求
#如果你向同一個伺服器發起多個請求,可以改為透過 Client 保持持久連線,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 資料以 map 結構表示,並且在 JSON 中,map 的鍵始終是字串,因此可以將 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 序列化和反序列化的資訊,包括自動生成轉換邏輯,請參閱使用 JSON 指南。
將響應轉換為結構化類的物件
#現在你有了儲存資料的類和一種將解碼後的 JSON 物件轉換為該型別物件的方法。接下來,可以編寫一個函式將所有內容整合起來
- 根據傳入的包名稱建立你的
URI。 - 使用
http.get獲取該包的資料。 - 如果請求不成功,丟擲
Exception,或者最好是自定義的Exception子類。 - 如果請求成功,使用
json.decode解碼響應體。 - 使用你建立的
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();
}
}
下一步是什麼?
#現在你已經從網際網路獲取、解析並使用了資料,可以考慮學習更多關於 Dart 中的併發。如果你的資料量大且複雜,可以將資料獲取和解碼移到另一個 isolate 中作為後臺工作者來防止你的介面無響應。