跳到主内容

从互联网获取数据

大多数应用程序需要某种形式的通信或从互联网获取数据。许多应用程序通过 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 版本。
  • 向服务器提供额外信息的 Header(头部)字段。
  • 一个可选的 Body(主体),以便请求可以向服务器发送数据,而不仅仅是检索数据。

要了解更多关于 HTTP 协议的信息,请查阅 mdn web docs 上的HTTP 概述

URI 与 URL

#

要发起 HTTP 请求,你需要提供一个指向资源的 URI (统一资源标识符)。URI 是唯一标识资源的字符串。URL (统一资源定位符) 是一种特殊的 URI,它同时提供了资源的位置。Web 资源的 URL 包含三个信息片段。对于当前页面,URL 由以下部分组成:

  • 用于确定所使用协议的方案 (scheme): https
  • 服务器的权威机构或主机名 (authority or hostname): dart.dev
  • 资源的路径 (path): /tutorials/server/fetch-data.html

还有一些当前页面未使用的可选参数

  • 用于自定义额外行为的参数 (parameters): ?key1=value1&key2=value2
  • 一个锚点 (anchor),不会发送到服务器,指向资源中的特定位置: #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
// 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"
}

请注意数据的结构(在本例中是一个 map),稍后在解码 JSON 时会用到它。

如果需要从响应中获取其他信息,例如状态码Header(头部),可以使用顶级函数 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 Header(头部)。可以通过将键值对的 Map<String, String> 作为可选的命名参数 headers 传入来指定 Header。

dart
await http.get(
  Uri.https('dart.dev', '/f/packages/http.json'),
  headers: {'User-Agent': '<product name>/<product-version>'},
);

发起多个请求

#

如果你向同一个服务器发起多个请求,可以改为通过 Client 保持持久连接,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 数据以 map 结构表示,并且在 JSON 中,map 的键始终是字符串,因此可以将 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 解码响应体。
  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 中作为后台工作者来防止你的界面无响应。