内容

从互联网获取数据

大多数应用程序需要某种形式的通信或从互联网检索数据。许多应用程序通过 HTTP 请求来实现这一点,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 文档上的 HTTP 概述

URI 和 URL

#

要发出 HTTP 请求,您需要为资源提供 URI(统一资源标识符)。URI 是唯一标识资源的字符字符串。URL(统一资源定位符)是一种特殊的 URI,它还提供了资源的位置。Web 上资源的 URL 包含三部分信息。对于当前页面,URL 由以下部分组成:

  • 用于确定所用协议的方案:https
  • 服务器的授权或主机名:dart.dev
  • 资源的路径:/tutorials/server/fetch-data.html

还有其他可选参数,当前页面未使用。

  • 自定义额外行为的参数:?key1=value1&key2=value2
  • 锚点,不会发送到服务器,指向资源中的特定位置:#uris

要了解有关 URL 的更多信息,请参阅 mdn Web 文档上的 什么是 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.ac.cn/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 内置的dart:convert库中的json.decode函数,使用 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 序列化和反序列化的更多信息,包括自动生成转换逻辑,请参阅使用 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 中的并发。如果您的数据量大且复杂,您可以将检索和解码移动到另一个隔离区作为后台工作线程,以防止您的界面变得无响应。