目录

从互联网获取数据

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

通过 HTTP 通信的数据在技术上可以采用任何形式, 但使用 JSON (JavaScript 对象表示法) 是一个流行的选择,因为它易于阅读且与语言无关。 Dart SDK 和生态系统也广泛支持 JSON, 并提供多种选项以最佳方式满足您的应用程序需求。

在本教程中, 您将了解有关 HTTP 请求、URI 和 JSON 的更多信息。 然后,您将学习如何使用 package:http 以及 dart:convert 库中的 Dart 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, 它也提供了资源的位置。 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 来标识 正在请求的资源 或正在访问的端点。

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

以下代码段显示了两种方法, 用于创建一个指向此站点上托管的关于 package:http 的模拟 JSON 格式信息的 Uri 对象:

dart
// 解析整个 URI,包括方案
Uri.parse('https://dart.dev/f/packages/http.json');

// 特别是使用 https 方案创建一个 URI
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('未能检索 http 包!');
    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 对象。

通过手动编写 fromJson 方法来转换解码后的 JSON, 该方法与之前的 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 (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('关于 $packageName 包的信息:');
  print('最新版本:${packageInfo.latestVersion}');
  print('描述:${packageInfo.description}');
  print('发布者:${packageInfo.publisher}');

  final repository = packageInfo.repository;
  if (repository != null) {
    print('代码库:$repository');
  }
}

Future<PackageInfo> getPackage(String packageName) async {
  final packageUrl = Uri.https('dart.dev', '/f/packages/$packageName.json');
  final packageResponse = await http.get(packageUrl);

  // 如果请求不成功,则抛出异常
  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('未能检索包 $packageName 信息');

    if (statusCode != null) {
      buf.write(',状态码为 $statusCode');
    }

    buf.write('!');
    return buf.toString();
  }
}

接下来的步骤?

#

现在您已经检索、解析并使用了 来自互联网的数据, 可以考虑学习有关 Dart 中的并发 的更多信息。 如果您的数据量很大且很复杂, 您可以将检索和解码 移动到另一个 隔离区 作为后台工作程序, 以防止界面变得无响应。