跳到主要内容

从互联网获取数据

大多数应用程序都需要某种形式的通信或从互联网检索数据。许多应用程序通过 HTTP 请求来实现这一点,这些请求从客户端发送到服务器,以对通过 URI(统一资源标识符)标识的资源执行特定操作。

通过 HTTP 通信的数据在技术上可以是任何形式,但使用 JSON(JavaScript 对象表示法)由于其人类可读性和语言无关性而成为一种流行的选择。Dart SDK 和生态系统也对 JSON 提供了广泛的支持,并提供多种选项以最好地满足你的应用程序的需求。

在本教程中,你将学习更多关于 HTTP 请求、URI 和 JSON 的知识。然后你将学习如何使用 package:http 以及 Dart 在 dart:convert 库中的 JSON 支持来获取、解码,然后使用从 HTTP 服务器检索的 JSON 格式的数据。

背景概念

#

以下部分提供了关于本教程中使用的技术和概念的一些额外背景和信息,以方便从服务器获取数据。要直接跳到教程内容,请参阅检索必要的依赖

JSON

#

JSON(JavaScript 对象表示法)是一种数据交换格式,已在应用程序开发和客户端-服务器通信中变得无处不在。它很轻量级,但由于基于文本,因此也易于人类阅读和编写。使用 JSON,各种数据类型和简单的数据结构(如列表和映射)可以被序列化并用字符串表示。

大多数语言都有许多实现,并且解析器已经变得非常快,因此你无需担心互操作性或性能。有关 JSON 格式的更多信息,请参阅 Introducing 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,它还提供资源的位置。Web 上资源的 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,该 URL 标识要请求的资源或要访问的端点。

在 Dart 中,URL 通过 Uri 对象表示。有很多方法可以构建 Uri,但由于其灵活性,使用 Uri.parse 解析字符串来创建一个是很常见的解决方案。

以下代码段展示了创建 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 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 内置的 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 中的并发的信息。如果你的数据很大且很复杂,你可以将检索和解码移动到另一个 isolate 作为后台工作程序,以防止你的界面变得无响应。