跳到主內容

從網際網路獲取資料

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

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 格式資訊的 Uri 物件的方法

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

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 時會用到它。

如果需要從響應中獲取其他資訊,例如狀態碼Header(頭部),可以使用頂級函式 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 Header(頭部)。可以透過將鍵值對的 Map<String, String> 作為可選的命名引數 headers 傳入來指定 Header。

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

發起多個請求

#

如果你向同一個伺服器發起多個請求,可以改為透過 Client 保持持久連線,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 資料以 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 方法來轉換解碼後的 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 解碼響應體。
  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();
  }

}

下一步是什麼?

#

現在你已經從網際網路獲取、解析並使用了資料,可以考慮學習更多關於 Dart 中的併發。如果你的資料量大且複雜,可以將資料獲取和解碼移到另一個 isolate 中作為後臺工作者來防止你的介面無響應。