跳到主要內容

從網際網路擷取資料

大多數應用程式都需要某種形式的通訊或從網際網路擷取資料。許多應用程式透過 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,請匯入它,並可選擇指定程式庫前綴

dart
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 格式資訊

dart
// 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 格式資訊作為字串,然後將其列印出來

dart
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 查看。

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 函式,它會傳回帶有 ResponseFuture

以下程式碼片段使用 get 取得完整回應,以便在請求不成功時提前退出,這以狀態碼 200 表示

dart
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 可選的命名參數來指定標頭

dart
await http.get(
  Uri.https('dart.dev', '/f/packages/http.json'),
  headers: {'User-Agent': '<product name>/<product-version>'},
);

發出多個請求

#

如果您要對同一伺服器發出多個請求,您可以改為透過 Client 保留持久連線,它具有與頂層方法類似的方法。只需確保在完成後使用 close 方法清理。

dart
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

dart
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>

dart
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 之外的所有欄位都是必需的,並且每次都提供。

dart
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 欄位

dart
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 物件轉換為該類型的物件。接下來,您可以編寫一個將所有內容整合在一起的函式

  1. 根據傳入的套件名稱建立您的 URI
  2. 使用 http.get 擷取該套件的資料。
  3. 如果請求未成功,則拋出 Exception 或最好是您自己的自訂 Exception 子類別。
  4. 如果請求成功,則使用 json.decode 將回應本文解碼為 JSON 字串。
  5. 使用您建立的 PackageInfo.fromJson 工廠建構子將解碼後的 JSON 字串轉換為 PackageInfo 物件。
dart
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,或在 WebFlutter 應用程式中顯示它。

這是一個完整的、可執行的範例,它請求、解碼,然後顯示關於 httppath 套件的模擬資訊

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 作為背景工作程式,以防止您的介面變得無回應。