目录

从互联网获取数据

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

通过 HTTP 传输的数据在技术上可以是任何形式,但由于 JSON(JavaScript 对象表示法)具有人类可读性和语言无关的特性,因此使用 JSON 是一种流行的选择。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 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 解析字符串来创建一个是很常见的解决方案。

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

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 交互的其他方法的更多信息,请参阅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 作为后台工作器,以防止你的界面变得无响应。