概述

REST API 在许多方面非常简单。存在输入,称为请求。输入由服务器解释并生成输出。输出被称为响应。在某些方面,你可以将向 WordPress REST API 发出的请求视为一组应由 API 执行和解释的指令或说明。默认情况下,WordPress REST API 旨在使用 HTTP 请求作为其请求介质。HTTP 是互联网上数据通信的基础,这使得 WordPress REST API 成为一个非常广泛的 API。API 中的请求利用了 HTTP 请求中存在的许多不同方面,如 URI、HTTP 方法、标头和参数。请求的数据结构由 WP_REST_Request 类方便地处理。

WP_REST_Request

该类是 WordPress 4.4 中引入的三个主要基础设施类之一。当向 API 的端点发出 HTTP 请求时,API 将自动创建 WP_REST_Request 类的实例,以匹配提供的数据。响应对象在 WP_REST_Server 的 serve_request() 方法中自动生成。一旦创建了请求并检查了身份验证,请求就会被分发,我们的端点回调开始被触发。所有存储在 WP_REST_Request 对象中的数据都会传递给为我们注册的端点的回调函数。因此,我们的 permissions_callback 和 callback 都会在传入请求对象时被调用。这使得我们能够在回调中访问各种请求属性,从而定制我们的响应以匹配所需的输出。

请求属性

请求对象具有许多不同的属性,每个属性都可以以各种方式使用。主要属性是请求方法、路由、标头、参数和属性。让我们逐一分析它们在请求中的作用。如果你自己创建一个请求对象,它将如下所示:

$request = new WP_REST_Request( 'GET', '/my-namespace/v1/examples' );

在上述代码示例中,我们仅指定请求对象的方法是 GET,并且我们应该匹配路由 /my-namespace/v1/examples,在完整 URL 的上下文中,它将如下所示:https://ourawesomesite.com/wp-json/my-namepsace/v1/examples。WP_REST_Request 构造函数的方法和路由参数用于将请求映射到所需的端点。如果请求发往未注册的端点,则会在响应中返回有用的 404 错误消息。让我们更深入地查看各种属性。

方法

请求对象的方法属性默认匹配 HTTP 请求方法。在大多数情况下,方法将是 GET、POST、PUT、DELETE、OPTIONS 或 HEAD 之一。这些方法将用于匹配注册到路由的各种端点。当 API 找到方法和路由的匹配项时,它将触发该端点的回调。

以下约定是匹配 HTTP 方法的最佳实践:GET 用于只读任务,POST 用于创建,PUT 用于更新,DELETE 用于删除。请求方法作为指示你的端点预期功能的标志。当你向路由发出 GET 请求时,你应该期望返回只读数据。

路由

请求的路由默认将匹配服务器环境变量中的路径信息;$_SERVER['PATH_INFO']。当你向 WordPress REST API 的路由发出 HTTP 请求时,生成的 WP_REST_Request 对象将被设置为匹配该路径,希望随后将其匹配到有效的端点。简而言之,请求的路由是你希望在 API 中针对请求的位置。

如果我们注册了一个使用 GET 的书籍端点,它可能位于 https://ourawesomesite.com/wp-json/my-namespace/v1/books。如果我们在浏览器中访问该 URL,我们将看到以 JSON 格式表示的书籍集合。WordPress 将自动为我们生成请求对象并处理所有路由以匹配端点。因此,由于我们不必担心路由本身,了解如何在请求中传递额外数据是更重要的事情。

标头

HTTP 请求标头只是关于我们 HTTP 请求的额外数据。请求标头可以指定缓存策略、我们的请求内容是什么、请求来自何处以及许多其他内容。请求标头不一定与我们的端点直接交互,但标头中的信息有助于 WordPress 知道该做什么。要传递我们希望端点与之交互的数据,我们想使用参数。

参数

在向 WordPress REST API 发出请求时,大多数传入的附加数据将采用参数的形式。什么是参数?在 API 的上下文中,有四种不同类型。有路由参数、查询参数、体参数和文件参数。让我们更深入地查看每一种。

URL 参数

URL 参数是从请求路由中的路径变量自动生成的 WP_REST_Request。这意味着什么?让我们看看这个路由,它通过 ID 获取单个书籍:/my-namespace/v1/books/(?P\d+)。看起来奇怪的 (?P\d+) 是一个路径变量。路径变量的名称是'id'。

如果我们发出像 GET https://ourawesomesite.com/wp-json/my-namespace/v1/books/5 这样的请求,5 将成为我们的 id 路径变量的值。WP_REST_Request 对象将自动采用该路径变量并将其存储为 URL 参数。现在在我们的端点回调内部,我们可以非常容易地与该 URL 参数交互。让我们看看一个示例。

// Register our individual books endpoint.
function prefix_register_book_route() {
    register_rest_route( 'my-namespace/v1', '/books/(?P<id>\d+)', array(
        // Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET.
        'methods' => WP_REST_Server::READABLE,
        // Register the callback for the endpoint.
        'callback' => 'prefix_get_book',
    ) );
}

add_action( 'rest_api_init', 'prefix_register_book_route' ); 

/**
 * Our registered endpoint callback. Notice how we are passing in $request as an argument.
 * By default, the WP_REST_Server will pass in the matched request object to our callback.
 *
 * @param WP_REST_Request $request The current matched request object.
 */
function prefix_get_book( $request ) {
    // Here we are accessing the path variable 'id' from the $request.
    $book = prefix_get_the_book( $request['id'] );
    return rest_ensure_response( $book );
}

// A simple function that grabs a book title from our books by ID.
function prefix_get_the_book( $id ) {
    $books = array(
        'Design Patterns',
        'Clean Code',
        'Refactoring',
        'Structure and Interpretation of Computer Programs',
    );

    $book = '';
    if ( isset( $books[ $id ] ) ) {
        // Grab the matching book.
        $book = $books[ $id ];
    } else {
        // Error handling.
        return new WP_Error( 'rest_not_found', esc_html__( 'The book does not exist', 'my-text-domain' ), array( 'status' => 404 ) );
    }

    return $book;
}

在上述示例中,我们看到路径变量如何作为 URL 参数存储在请求对象中。然后我们可以在端点回调中访问这些参数。上述示例是使用 URL 参数的一个相当常见的用例。向路由添加太多路径变量会减慢路由匹配的速度,并且也会使注册端点过于复杂,建议谨慎使用 URL 参数。如果我们不应该在 URL 路径中直接使用参数,那么我们需要另一种方式将额外信息传递给请求。这就是查询和体参数发挥作用的地方,它们通常会在你的 API 中承担大部分工作。

查询参数

查询参数存在于 URI 的查询字符串部分。https://ourawesomesite.com/wp-json/my-namespace/v1/books?per_page=2&genre=fiction 的 URI 的查询字符串部分是 ?per_page=2&genre=fiction。查询字符串以'?'字符开头,查询字符串中的不同值由'&'字符分隔。我们在查询字符串中指定了两个参数;per_page 和 fiction。在我们的端点中,我们只想从虚构类中获取两本书。我们可以像这样在回调中访问这些值:$request['per_page'] 和 $request['genre'](假设 $request 是我们使用的参数的名称)。如果你熟悉 PHP,你可能已经在 Web 应用程序中使用过查询参数。

在 PHP 中,查询参数存储在超全局变量 $_GET 中。重要的是要注意,您永远不应直接在您的端点中访问任何超全局变量或服务器变量。最好始终使用 WP_REST_Request 类提供的功能。另一种向端点传递变量的常用方法是使用体参数。

体参数

体参数是存储在请求体中的键值对。如果您曾经通过 、cURL 或其他方法发送过 POST 请求,那么您就已经使用过体参数。使用体参数时,您可以以不同的内容类型传递它们。POST 请求的默认 Content-Type 标头是 x-www-form-urlencoded。当使用 x-www-form-urlencoded 时,参数的发送方式类似于查询字符串;per_page=2&genre=fiction。HTML 表单默认会将各种输入捆绑在一起并发送一个匹配 x-www-form-urlencoded 模式的 POST 请求。

重要的是要注意,虽然 HTTP 规范并不禁止在 GET 请求中使用体参数,但鼓励您不要在 GET 请求中使用体参数。体参数可以并且应该用于 POST、PUT 和 DELETE 请求。

文件参数

WP_REST_Request 对象中的文件参数在请求使用特殊内容类型标头时存储;multipart/form-data。然后可以使用 $request->get_file_params() 从请求对象访问文件数据。文件参数等同于 PHP 超全局变量:$_FILES。请记住,不要直接访问超全局变量,只使用 WP_REST_Request 对象提供的功能。

在端点回调中,我们可以使用 wp_handle_upload() 将所需的文件添加到 WordPress 的媒体上传目录。文件参数仅用于处理文件数据,您永远不应将其用于任何其他目的。

属性

WP_REST_Request 还支持请求属性。请求的属性是注册到匹配路由的路由属性。如果我们向 my-namespace/v1/books 发送一个 GET 请求,然后在端点回调中调用 $request->get_attributes(),我们将返回 my-namespace/v1/books 端点的注册选项。如果我们向同一路由发送 POST 请求,并且我们的端点回调也返回 $request->get_attributes(),我们将收到一组不同的注册到 POST 端点回调的端点选项。

在属性中,我们将获得一个包含支持的方法、选项、是否在此索引中显示此端点、端点的注册参数列表以及我们注册的回调的响应。它可能看起来像这样:

{
  "methods": {
    "GET": true
  },
  "accept_json": false,
  "accept_raw": false,
  "show_in_index": true,
  "args": {
    "context": {
      "description": "请求所在的范围;确定响应中存在的字段。",
      "type": "string",
      "sanitize_callback": "sanitize_key",
      "validate_callback": "rest_validate_request_arg",
      "enum": [
        "view",
        "embed",
        "edit"
      ],
      "default": "view"
    },
    "page": {
      "description": "集合的当前页。",
      "type": "integer",
      "default": 1,
      "sanitize_callback": "absint",
      "validate_callback": "rest_validate_request_arg",
      "minimum": 1
    },
    "per_page": {
      "description": "结果集中返回的最大项目数。",
      "type": "integer",
      "default": 10,
      "minimum": 1,
      "maximum": 100,
      "sanitize_callback": "absint",
      "validate_callback": "rest_validate_request_arg"
    },
    "search": {
      "description": "将结果限制为匹配字符串的项目。",
      "type": "string",
      "sanitize_callback": "sanitize_text_field",
      "validate_callback": "rest_validate_request_arg"
    },
    "after": {
      "description": "将响应限制为在给定符合 ISO8601 的日期之后发布的资源。",
      "type": "string",
      "format": "date-time",
      "validate_callback": "rest_validate_request_arg"
    },
    "author": {
      "description": "将结果集限制为分配给特定作者的帖子。",
      "type": "array",
      "default": [],
      "sanitize_callback": "wp_parse_id_list",
      "validate_callback": "rest_validate_request_arg"
    },
    "author_exclude": {
      "description": "确保结果集排除分配给特定作者的帖子。",
      "type": "array",
      "default": [],
      "sanitize_callback": "wp_parse_id_list",
      "validate_callback": "rest_validate_request_arg"
    },
    "before": {
      "description": "将响应限制为在给定符合 ISO8601 的日期之前发布的资源。",
      "type": "string",
      "format": "date-time",
      "validate_callback": "rest_validate_request_arg"
    },
    "exclude": {
      "description": "确保结果集排除特定 ID。",
      "type": "array",
      "default": [],
      "sanitize_callback": "wp_parse_id_list"
    },
    "include": {
      "description": "将结果集限制为特定 ID。",
      "type": "array",
      "default": [],
      "sanitize_callback": "wp_parse_id_list"
    },
    "offset": {
      "description": "通过特定数量的项目偏移结果集。",
      "type": "integer",
      "sanitize_callback": "absint",
      "validate_callback": "rest_validate_request_arg"
    },
    "order": {
      "description": "按升序或降序对排序属性进行排序。",
      "type": "string",
      "default": "desc",
      "enum": [
        "asc",
        "desc"
      ],
      "validate_callback": "rest_validate_request_arg"
    },
    "orderby": {
      "description": "按对象属性对集合进行排序。",
      "type": "string",
      "default": "date",
      "enum": [
        "date",
        "relevance",
        "id",
        "include",
        "title",
        "slug"
      ],
      "validate_callback": "rest_validate_request_arg"
    },
    "slug": {
      "description": "将结果集限制为具有特定 slugs 的帖子。",
      "type": "string",
      "validate_callback": "rest_validate_request_arg"
    },
    "status": {
      "default": "publish",
      "description": "将结果集限制为分配了特定状态的帖子;可以是状态类型的逗号分隔列表。",
      "enum": [
        "publish",
        "future",
        "draft",
        "pending",
        "private",
        "trash",
        "auto-draft",
        "inherit",
        "any"
      ],
      "sanitize_callback": "sanitize_key",
      "type": "string",
      "validate_callback": [
        {},
        "validate_user_can_query_private_statuses"
      ]
    },
    "filter": {
      "description": "使用 WP 查询参数修改响应;私有查询变量需要适当的授权。"
    },
    "categories": {
      "description": "将结果集限制为在分类法中分配了指定术语的所有项目。",
      "type": "array",
      "sanitize_callback": "wp_parse_id_list",
      "default": []
    },
    "tags": {
      "description": "将结果集限制为在标签分类法中分配了指定术语的所有项目。",
      "type": "array",
      "sanitize_callback": "wp_parse_id_list",
      "default": []
    }
  },
  "callback": [
    {},
    "get_items"
  ],
  "permission_callback": [
    {},
    "get_items_permissions_check"
  ]
}

如您所见,我们已注册到端点的所有信息都已在那里,随时可用!请求属性通常在较低级别使用,并由 WP_REST_Server 类处理,但在端点回调中可以做一些很酷的事情,例如限制接受的参数以匹配注册的参数。

WP REST API 的设计是为了让您无需折腾任何内部功能,因此与 WP_REST_Request 交互的一些更高级的方法并不常见。使用 WP REST API 的核心在于注册路由和端点。请求是我们用来告诉 API 我们要访问哪个端点的工具。这通常是通过 HTTP 完成的,但我们也可以在内部使用 WP_REST_Request。

内部请求

进行内部请求的关键是使用 rest_do_request()。您只需要传递一个请求对象,就会返回一个响应。由于请求从未由 WP_REST_Server 提供,因此响应数据永远不会编码为 json,这意味着我们拥有作为 PHP 对象的响应对象。这非常棒,使我们能够做很多有趣的事情。首先,我们可以创建高效的批量端点。从性能角度来看,其中一个障碍是最小化 HTTP 请求。我们可以创建批量端点,使用 rest_do_request() 在一个 HTTP 请求中内部提供所有请求。这是一个非常简化的只读数据批量端点,以便您可以看到 rest_do_request() 的作用。

// 注册我们的模拟批量端点。
function prefix_register_batch_route() {
    register_rest_route( 'my-namespace/v1', '/batch', array(
        // 此端点支持的方法。WP_REST_Server::READABLE 对应 GET。
        'methods' => WP_REST_Server::READABLE,
        // 注册端点的回调函数。
        'callback' => 'prefix_do_batch_request',
        // 注册批量端点的参数。
        'args' => prefix_batch_request_parameters(),
    ) );
}

add_action( 'rest_api_init', 'prefix_register_batch_route' );

/**
 * 我们注册的端点回调函数。注意我们如何将 $request 作为参数传入。
 * 默认情况下,WP_REST_Server 会将匹配到的请求对象传递给我们的回调函数。
 *
 * @param WP_REST_Request $request 当前匹配到的请求对象。
 */
function prefix_do_batch_request( $request ) {
    // 在此处初始化将存放响应数据的数组。
    $data = array();
	$data = prefix_handle_batch_requests( $request['requests'] );
	return $data;
}

/**
 * 此函数处理为我们发出的批量请求构建响应。
 *
 * @param array $requests 用于构建 WP_REST_Request 对象的数组数据。
 * @return WP_REST_Response 批量端点的响应数据集合。
 */
function prefix_handle_batch_requests( $requests ) {
	$data = array();

	// 遍历 requests 参数中指定的每个请求并运行端点。
	foreach ( $requests as $request_params ) {
		$response = prefix_handle_request( $request_params );
		$key = $request_params['method'] . ' ' . $request_params['route'];
		$data[ $key ] = prefix_prepare_for_collection( $response );
	}

	return rest_ensure_response( $data );
}

/**
 * 此函数处理为我们发出的批量请求构建响应。
 *
 * @param array $request_params 用于构建 WP_REST_Request 对象的数据。
 * @return WP_REST_Response 请求的响应数据。
 */
function prefix_handle_request( $request_params ) {
	$request = new WP_REST_Request( $request_params['method'], $request_params['route'] );

	// 将指定的请求参数添加到请求中。
	if ( isset( $request_params['params'] ) ) {
		foreach ( $request_params['params'] as $param_name => $param_value ) {
			$request->set_param( $param_name, $param_value );
		}
	}
	$response = rest_do_request( $request );
	return $response;
}

/**
 * 准备响应以便插入到响应集合中。
 *
 * 此代码取自 WP REST API v2 插件中的 WP_REST_Controller 类。
 *
 * @param WP_REST_Response $response 响应对象。
 * @return array 响应数据,已准备好插入到集合数据中。
 */
function prefix_prepare_for_collection( $response ) {
	if ( ! ( $response instanceof WP_REST_Response ) ) {
		return $response;
	}

	$data = (array) $response->get_data();
	$server = rest_get_server();

	if ( method_exists( $server, 'get_compact_response_links' ) ) {
		$links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
	} else {
		$links = call_user_func( array( $server, 'get_response_links' ), $response );
	}

	if ( ! empty( $links ) ) {
		$data['_links'] = $links;
	}

	return $data;
}

/**
 * 返回我们注册的参数的 JSON schema 数据。
 *
 * @return array $params JSON Schema 数据的 PHP 表示形式。
 */
function prefix_batch_request_parameters() {
    $params = array();

    $params['requests'] = array(
		'description'        => esc_html__( '一个请求对象参数数组,这些参数可以构建为 WP_REST_Request 实例。', 'my-text-domain' ),
		'type'               => 'array',
		'required'           => true,
		'validate_callback'  => 'prefix_validate_requests',
		'items'              => array(
			array(
				'type' => 'object',
				'properties' => array(
					'method' => array(
						'description' => esc_html__( '所需请求的 HTTP 方法。', 'my-text-domain' ),
						'type'        => 'string',
						'required'    => true,
						'enum'        => array(
							'GET',
							'POST',
							'PUT',
							'DELETE',
							'OPTIONS',
						),
					),
					'route' => array(
						'description' => esc_html__( '请求所需的路线。', 'my-text-domain' ),
						'required'    => true,
						'type'        => 'string',
						'format'      => 'uri',
					),
					'params' => array(
						'description' => esc_html__( '所需请求参数的键值对。', 'my-text-domain' ),
						'type' => 'object',
					),
				),
			),
		),
	);

	return $params;
}

function prefix_validate_requests( $requests, $request, $param_key ) {
	// 如果 requests 不是请求数组,则不处理批量请求。
	if ( ! is_array( $requests ) ) {
		return new WP_Error( 'rest_invald_param', esc_html__( 'requests 参数必须是请求数组。', 'my-text-domain' ), array( 'status' => 400 ) );
	}

	foreach ( $requests as $request ) {
		// 如果未设置方法或路线,则不运行请求。
		if ( ! isset( $request['method'] ) || ! isset( $request['route'] ) ) {
			return new WP_Error( 'rest_invald_param', esc_html__( '必须为每个请求指定方法和路线。', 'my-text-domain' ), array( 'status' => 400 ) );
		}

		if ( isset( $request['params'] ) && ! is_array( $request['params'] ) ) {
			return new WP_Error( 'rest_invald_param', esc_html__( '必须为每个请求指定参数,格式为命名键值对数组。', 'my-text-domain' ), array( 'status' => 400 ) );
		}
	}

	// 这是一种数据验证的黑名单方法。
	return true;
}

这是一大段涵盖多个主题的代码,但所有内容都围绕 prefix_handle_request() 中发生的事情展开。在此处,我们传入一个数组,该数组告诉我们 HTTP 方法、路线以及一组我们要将其转换为请求的参数。然后我们为方法和路线构建请求对象。如果指定了任何参数,我们使用 WP_REST_Request::set_param() 方法添加所需的参数。一旦我们的 WP_REST_Request 准备就绪,我们就使用 rest_do_request 在内部匹配该端点,并将响应返回到批量端点响应集合中。使用此类批量端点可以带来巨大的性能提升,因为您只需进行一次 HTTP 请求即可获取多个端点的响应。此实现不一定是最优的,仅作为示例;并非执行此操作的唯一方法。