目錄

從網路擷取資料

大多數應用程式都需要某種形式的通訊或從網際網路擷取資料。許多應用程式透過 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 版本。
  • 向伺服器提供額外資訊的標頭。
  • 一個選用的主體,以便請求可以將資料傳送到伺服器,而不僅僅是擷取資料。

若要深入了解 HTTP 協定,請查看 mdn web docs 上的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 web docs 上的什麼是 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('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"
}

請注意資料的結構(在此情況下為一個 map),因為您稍後在解碼 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 回應狀態碼

某些伺服器請求需要更多資訊,例如身份驗證或使用者代理資訊;在這種情況下,您可能需要包含 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:convert 程式庫中 Dart 的內建 json.decode 函式,將原始字串轉換為使用 Dart 物件的 JSON 表示法。在此範例中,JSON 資料以 map 結構表示,而在 JSON 中,map 的鍵始終是字串,因此您可以將 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 方法,在必要時強制轉型類型並處理可選的 repository 欄位,來轉換解碼後的 JSON。

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 中的並發。如果您的資料很大且複雜,您可以將檢索和解碼移至另一個隔離區作為背景工作程式,以防止您的介面變得無回應。