内容

从互联网获取数据

大多数应用程序都需要某种形式的通信或从互联网检索数据。许多应用通过 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 web 文档中的 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 对象。

通过手动编写与早期 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,或在 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 中的并发性。如果您的数据庞大且复杂,您可以将检索和解码移至另一个 隔离区 作为后台工作程序,以防止您的界面变得无响应。