从互联网获取数据
大多数应用程序需要某种形式的通信或从互联网获取数据。许多应用程序通过 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
,请导入它并可选择指定一个库前缀
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
对象的方法
// 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 格式信息作为字符串获取,然后将其打印出来
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
查看。
{
"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
,它返回一个包含 Response
的 Future
。
以下代码片段使用 get
获取整个响应,以便在请求不成功时提前退出,请求成功由状态码 200 表示。
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。
await http.get(
Uri.https('dart.dev', '/f/packages/http.json'),
headers: {'User-Agent': '<product name>/<product-version>'},
);
发起多个请求
#如果你向同一个服务器发起多个请求,可以改为通过 Client
保持持久连接,Client
具有与顶级函数类似的方法。完成后,请确保使用 close
方法进行清理。
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
中
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>
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
字段之外的所有字段都是必需的,并且每次都会提供。
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
字段
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 对象转换为该类型对象的方法。接下来,可以编写一个函数将所有内容整合起来
- 根据传入的包名称创建你的
URI
。 - 使用
http.get
获取该包的数据。 - 如果请求不成功,抛出
Exception
,或者最好是自定义的Exception
子类。 - 如果请求成功,使用
json.decode
解码响应体。 - 使用你创建的
PackageInfo.fromJson
工厂构造函数将解码后的 JSON 数据转换为PackageInfo
对象。
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),或在 Web 或 Flutter 应用中显示。
这里有一个完整且可运行的示例,它请求、解码,然后显示关于 http
和 path
包的模拟信息
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 中作为后台工作者来防止你的界面无响应。