內容

從網際網路擷取資料

大部分應用程式都需要從網際網路進行某種形式的通訊或資料擷取。許多應用程式會透過 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 (超文字傳輸協定) 是一種無狀態協定,設計用於傳輸文件,最初用於網路用戶端和網路伺服器之間。您與協定互動以載入此頁面,因為您的瀏覽器使用 HTTP GET 要求從網路伺服器擷取頁面的內容。自推出以來,HTTP 協定及其各種版本的用途已擴展到網路以外的應用程式,基本上只要需要從用戶端與伺服器通訊的地方。

從用戶端傳送以與伺服器通訊的 HTTP 要求由多個元件組成。HTTP 函式庫(例如 package:http)讓您可以指定下列種類的通訊

  • 定義所需動作的 HTTP 方法,例如用於擷取資料的 GET 或用於提交新資料的 POST
  • 透過 URI 定位資源。
  • 正在使用的 HTTP 版本。
  • 提供額外資訊給伺服器的標頭。
  • 一個選用主體,因此要求可以傳送資料給伺服器,而不仅仅是擷取資料。

如需深入了解 HTTP 協定,請查看 mdn 網路文件中的 HTTP 概觀

URI 和 URL

#

如需進行 HTTP 要求,您需要提供 URI (統一資源識別碼) 給資源。URI 是唯一識別資源的字元字串。URL (統一資源定位器) 是一種特定類型的 URI,也提供資源的位置。網路資源的 URL 包含三項資訊。對於此目前頁面,URL 由下列部分組成

  • 用於決定所用協定的架構:https
  • 伺服器的授權或主機名稱:dart.dev
  • 資源路徑:/tutorials/server/fetch-data.html

還有其他選用參數,但目前頁面並未使用

  • 用於自訂額外行為的參數:?key1=value1&key2=value2
  • 一個未傳送至伺服器的錨點,指向資源中的特定位置:#uris

若要深入了解 URL,請參閱 mdn 網路文件中的 什麼是 URL?

取得必要的相依性

#

package:http 函式庫提供跨平台的解決方案,可進行可組合的 HTTP 要求,並可選擇進行細緻的控制。

若要新增對 package:http 的依賴關係,請從儲存庫頂端執行下列 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 是常見的解決方案。

下列程式片段顯示兩種方法,用於建立一個 Uri 物件,指向本網站上關於 package:http 的模擬 JSON 格式資訊

dart
// Parse the entire URI, including the scheme
Uri.parse('http://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 網路文件中的 HTTP 回應狀態碼

有些伺服器要求需要更多資訊,例如驗證或使用者代理資訊;在這種情況下,您可能需要包含 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 物件。

透過手動撰寫 fromJson 方法來轉換已解碼的 JSON,比對先前的 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 序列化和反序列化,包括自動產生轉換邏輯,請參閱 使用 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,或在 網路Flutter 應用程式中顯示它。

以下是一個完整的可執行範例,它會要求、解碼,然後顯示關於 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();
  }
}

接下來呢?

#

現在您已從網際網路擷取、剖析和使用資料,請考慮深入瞭解 Dart 中的並行處理。如果您的資料龐大且複雜,您可以將擷取和解碼移至另一個 隔離,作為背景工作人員,以防止您的介面沒有回應。