博客

🦸🏻‍♂️ 介绍:无需 WordPress 的 Headless WordPress

Leonardo Losoviz
作者:Leonardo Losoviz ·

自 Matt Mullenweg 与 WPEngine 的风波以来,我注意到越来越多的人在 Reddit(以及其他地方)寻找 WordPress 的替代方案。他们未必是想立刻离开 WordPress,而是想了解自己有哪些选择,以及潜在的迁移会有多麻烦。他们想知道如何分散风险。

对于正在使用 Headless WordPress 的朋友们,Gato GraphQL 现在提供了一项酷炫的新功能:无需 WordPress 的 Headless WordPress

本文将详细介绍这一功能是如何实现的,并通过一段演示视频加以展示。

将 Gato GraphQL 作为独立 PHP 应用运行

Gato GraphQL 基于由 Composer 管理的独立 PHP 组件构建,构成 GraphQL 服务器的所有 PHP 组件均不依赖 WordPress!

因此,GraphQL 服务器可以作为独立 PHP 应用程序运行,您可以将其集成到任何 PHP 应用中,无论是基于 WordPress 还是其他框架。

如果某个用例不需要访问 WordPress 数据,那么至少对于该用例,您已经可以直接使用了。

以下视频演示了这样一个用例:与 GitHub API 交互,在开发过程中从 GitHub Actions 下载并安装构建产物:

无需 WordPress 的 Headless WordPress 演示:执行 GraphQL Query

在视频中,GraphQL query 执行了一个 HTTP 请求,获取 GitHub Actions 中生成的最新 Gato GraphQL 插件。这些插件在合并 Pull Request 时作为构建产物上传。

从 GraphQL 响应中获取的构建产物 URL 随后被注入到 WP-CLI,以便在本地 DEV 服务器上自动安装插件并运行测试。

(本文最后一节将进行更详细的说明。)

在此用例中,由于完全不需要访问 WordPress 数据,GraphQL 服务器已经可以作为独立 PHP 应用运行。

如果需要,我甚至可以在 GitHub Actions 工作流程中使用它!

迁移 Headless WordPress 应用

当您确实需要访问 WordPress 数据时,让我们看看如何在没有 WordPress 的情况下实现。

Gato GraphQL 提供的 GraphQL schema 包含用于获取 WordPress 数据的字段:文章、用户、评论、标签、分类等。

获取 WordPress 数据的 PHP 解析器代码依赖于 WordPress,这些代码无法在非 WordPress 应用中运行。

不过,Gato GraphQL 中的每个解析器都通过 2 个包实现:

  1. 「原生」PHP 版:包含所有通用代码
  2. WordPress 专用版:包含实际调用 WordPress 方法以满足该解析器需求的代码

例如,对于以下 GraphQL query:

{
  posts {
    id
    title
  }
}

...获取文章的逻辑由以下部分组成:

  1. Root.posts 字段:位于通用的 posts
  2. 通过 get_posts 方法为 WordPress 进行解析:位于 WordPress 专用的 posts-wp中。

非 WordPress 包与 WordPress 包之间的代码比例约为 80/20%,这意味着 80% 的代码可在其他框架/CMS 中复用,只有 20% 的代码需要重新实现

此外,Gato GraphQL 的所有功能均通过模块提供,模块可以随意启用或禁用。

Schema 模块
Schema 模块

Modules 是出于安全目的实现的功能:如果您不需要在公共 API 中公开用户数据,可以禁用 Users 模块,相应的字段(如 Root.users)将不会被添加到 schema 中。

模块直接映射到底层的 PHP 包。 因此,在将 Gato GraphQL 作为独立应用运行时,我们可以有选择地加载所需的模块/包,而不加载其他任何内容。

例如,如果您的应用只展示文章、分类和标签的数据,那么只需加载 posts-wpcategories-wptags-wp 包(及其依赖项)即可。

当您迁移离开 WordPress(例如迁移到 Laravel 或 Symfony)时,只需为新框架/CMS 重新实现这 3 个 WordPress 专用包,无需更改其他任何内容。

因此,您可以从今天开始使用 Headless WordPress,同时知晓未来可以以最小的代价将应用迁移到另一个框架或 CMS。

从其他 API 迁移到 Gato GraphQL

如果您已经在使用 Headless WordPress,您的应用很可能使用的是 WP REST API 或 WPGraphQL。

遗憾的是,使用这两种 API 中的任何一种,您都会被锁定在 WordPress 上:WordPress 之外没有 WP REST API,WPGraphQL 也无法在没有 WordPress 的情况下运行。

幸运的是,可以将其中任何一种替换为 Gato GraphQL,从而获得将 Headless WordPress 应用迁移离开 WordPress 的能力。

这需要以下 2 个步骤:

  1. 从 WP REST API 或 WPGraphQL 迁移到 Gato GraphQL
  2. 重新实现所需的 WordPress 专用包

让我们来看看如何完成 API 迁移。

WP REST API 迁移到 Gato GraphQL 的 persisted queries

使用 Persisted Queries 扩展,您可以发布由 GraphQL 构建的类 REST 端点。

对于应用中的每个 REST 端点,您可以创建一个对应的 persisted query 端点来获取相同的数据,然后改用该端点。

例如,以下 GraphQL query 可以替代 REST 端点 /wp-json/wp/v2/posts/

{
  posts {
    id
    date: dateStr(format: "Y-m-d\\TH:i:s")
    modified: modifiedDateStr(format: "Y-m-d\\TH:i:s")
    slug
    status
    link: url
    title: self {
      rendered: title
    }
    content: self {
      rendered: content
    },
    excerpt: self {
      rendered: excerpt
    }
    author
    featured_media: featuredImage
    sticky: isSticky
    categories
    tags
  }
}

借助 API 层级结构,persisted query 可以发布在路径 /graphql-query/wp/v2/posts/ 下,使端点映射变得简单。

要复制获取指定 ID 文章数据的 REST 端点 /wp-json/wp/v2/posts/{id}/,可以通过 URL 参数 postId 传入文章 ID。

例如,以下 persisted query 可以通过端点 /graphql-query/wp/v2/posts/single/?postId={id} 调用:

query GetPost($postId: ID!) {
  post(by: { id: $postId }) {
    id
    date: dateStr(format: "Y-m-d\\TH:i:s")
    modified: modifiedDateStr(format: "Y-m-d\\TH:i:s")
    slug
    status
    link: url
    title: self {
      rendered: title
    }
    content: self {
      rendered: content
    },
    excerpt: self {
      rendered: excerpt
    }
    author
    featured_media: featuredImage
    sticky: isSticky
    categories
    tags
  }
}

WPGraphQL 迁移到 Gato GraphQL

WPGraphQL 和 Gato GraphQL 的 GraphQL schema 相似但略有不同,因此需要进行适配。

Next.js WordPress 启动项目 leoloso/next-wordpress-starter 支持 WPGraphQL 和 Gato GraphQL 两种服务器。该启动项目对两种服务器使用相同的 JS 逻辑,只有 GraphQL queries 有所不同。

该启动项目提供了多个在两种服务器之间适配 queries 的示例。例如,这个 WPGraphQL query

fragment PostFields on Post {
  id
  categories {
    edges {
      node {
        databaseId
        id
        name
        slug
      }
    }
  }
  databaseId
  date
  isSticky
  postId
  slug
  title
}

...被这样适配为 Gato GraphQL 版本

fragment PostFields on Post {
  id
  categories: self {
    edges: categories(pagination: { limit: -1 }) {
      node: self {
        databaseId: id
        id
        name
        slug
      }
    }
  }
  databaseId: id
  date: dateStr
  isSticky
  postId: id
  slug
  title
}

详解:将 Gato GraphQL 作为独立 PHP 应用运行

以下是对前面演示视频的详细说明。

我们将要运行的 GraphQL query 保存在文件 retrieve-github-artifacts.gql 中。

该 query 通过从环境变量 GITHUB_ACCESS_TOKEN 获取访问令牌来连接 GitHub API。它根据提供的变量动态生成 actions/artifacts 端点的完整路径,然后向其发送 HTTP 请求。

从响应中,它提取每个构建产物条目中的「下载 URL」,并向它们发送异步 HTTP 请求。从每个「下载 URL」的 Location 响应头中,获取可下载文件的实际 URL。

最后,将所有 URL 以空格分隔输出,方便注入到 WP-CLI 中。

# File retrieve-github-artifacts.gql
 
query RetrieveProxyArtifactDownloadURLs(
  $repoOwner: String!
  $repoProject: String!
  $perPage: Int = 1
  $artifactName: String = ""
) {
  githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
    @remove
 
  # Create the authorization header to send to GitHub
  authorizationHeader: _sprintf(
    string: "Bearer %s"
    values: [$__githubAccessToken]
  )
    @remove
 
  # Create the authorization header to send to GitHub
  githubRequestHeaders: _echo(
    value: [
      { name: "Accept", value: "application/vnd.github+json" }
      { name: "Authorization", value: $__authorizationHeader }
    ]
  )
    @remove
    @export(as: "githubRequestHeaders")
 
  githubAPIEndpoint: _sprintf(
    string: "https://api.github.com/repos/%s/%s/actions/artifacts?per_page=%s&name=%s"
    values: [$repoOwner, $repoProject, $perPage, $artifactName]
  )
 
  # Use the field from "Send HTTP Request Fields" to connect to GitHub
  gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
    input: {
      url: $__githubAPIEndpoint
      options: { headers: $__githubRequestHeaders }
    }
  )
    @remove
 
  # Finally just extract the URL from within each "artifacts" item
  gitHubProxyArtifactDownloadURLs: _objectProperty(
    object: $__gitHubArtifactData
    by: { key: "artifacts" }
  )
    @underEachArrayItem(passValueOnwardsAs: "artifactItem")
      @applyField(
        name: "_objectProperty"
        arguments: { object: $artifactItem, by: { key: "archive_download_url" } }
        setResultInResponse: true
      )
    @export(as: "gitHubProxyArtifactDownloadURLs")
}
 
query CreateHTTPRequestInputs
  @depends(on: "RetrieveProxyArtifactDownloadURLs")
{
  httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
    @underEachArrayItem(passValueOnwardsAs: "url")
      @applyField(
        name: "_objectAddEntry"
        arguments: {
          object: {
            options: { headers: $githubRequestHeaders, allowRedirects: null }
          }
          key: "url"
          value: $url
        }
        setResultInResponse: true
      )
    @export(as: "httpRequestInputs")
    @remove
}
 
query RetrieveActualArtifactDownloadURLs
  @depends(on: "CreateHTTPRequestInputs")
{
  _sendHTTPRequests(inputs: $httpRequestInputs) {
    artifactDownloadURL: header(name: "Location")
      @export(as: "artifactDownloadURLs", type: LIST)
  }
}
 
query PrintSpaceSeparatedArtifactDownloadURLs
  @depends(on: "RetrieveActualArtifactDownloadURLs")
{
  spaceSeparatedArtifactDownloadURLs: _arrayJoin(
    array: $artifactDownloadURLs
    separator: " "
  )
}

PHP 逻辑直接从 Gato GraphQL 插件和「Power Extensions」包(发送 HTTP 请求等功能所需)中加载代码。

作为独立 PHP 应用,我们必须明确指定要初始化哪些模块,并提供任何非默认的配置。

例如,我们告知模块 SendHTTPRequests 允许连接到 https://api.github.com/repos,并告知模块 EnvironmentFields 允许访问环境变量 GITHUB_ACCESS_TOKEN

请注意,GraphQL schema 在第一次执行 GraphQL query 时生成,并缓存到磁盘。这样,从第二次起,计算 schema 的代码将不再执行,使执行速度更快。

最后,独立应用初始化 GraphQL 服务器,对其执行 query,并输出响应。

<?php
// File retrieve-github-artifacts.php
 
declare(strict_types=1);
 
use GraphQLByPoP\GraphQLServer\Server\StandaloneGraphQLServer;
use PoP\Root\Container\ContainerCacheConfiguration;
 
// Load the GraphQL server via the standalone PHP components
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql/vendor/scoper-autoload.php');
 
// Load the PRO extensions via the standalone PHP components
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql-power-extensions-bundle/vendor/scoper-autoload.php');
 
// Modules required in the GraphQL query
$moduleClasses = [
  \PoPSchema\EnvironmentFields\Module::class,
  \PoPSchema\FunctionFields\Module::class,
  \GraphQLByPoP\ExportDirective\Module::class,
  \GraphQLByPoP\DependsOnOperationsDirective\Module::class,
  \GraphQLByPoP\RemoveDirective\Module::class,
  \PoPSchema\ApplyFieldDirective\Module::class,
  \PoPSchema\SendHTTPRequests\Module::class,
  \PoPSchema\ConditionalMetaDirectives\Module::class,
  \PoPSchema\DataIterationMetaDirectives\Module::class,
];
 
// Configure the modules
$moduleClassConfiguration = [
  \PoP\GraphQLParser\Module::class => [
    \PoP\GraphQLParser\Environment::ENABLE_MULTIPLE_QUERY_EXECUTION => true,
    \PoP\GraphQLParser\Environment::USE_LAST_OPERATION_IN_DOCUMENT_FOR_MULTIPLE_QUERY_EXECUTION_WHEN_OPERATION_NAME_NOT_PROVIDED => true,
    \PoP\GraphQLParser\Environment::ENABLE_RESOLVED_FIELD_VARIABLE_REFERENCES => true,
    \PoP\GraphQLParser\Environment::ENABLE_COMPOSABLE_DIRECTIVES => true,
  ],
  \PoPSchema\SendHTTPRequests\Module::class => [
    \PoPSchema\SendHTTPRequests\Environment::SEND_HTTP_REQUEST_URL_ENTRIES => [
      '#https://api.github.com/repos/(.*)#',
    ],
  ],
  \PoPSchema\EnvironmentFields\Module::class => [
    \PoPSchema\EnvironmentFields\Environment::ENVIRONMENT_VARIABLE_OR_PHP_CONSTANT_ENTRIES => [
      'GITHUB_ACCESS_TOKEN',
    ],
  ],
];
 
// Cache the schema to disk, to speed-up execution from the 2nd time onwards
$containerCacheConfiguration = new ContainerCacheConfiguration('MyGraphQLServer', true, 'retrieve-github-artifacts', __DIR__ . '/tmp');
 
// Initialize the server
$graphQLServer = new StandaloneGraphQLServer($moduleClasses, $moduleClassConfiguration, [], [], $containerCacheConfiguration);
 
/**
 * GraphQL query to execute, stored in its own .gql file
 *
 * @var string
 */
$query = file_get_contents(__DIR__ . '/retrieve-github-artifacts.gql');
 
// GraphQL variables
$variables = [
  'repoOwner' => 'GatoGraphQL',
  'repoProject' => 'GatoGraphQL',
  'perPage' => 3
];
 
// Execute the query
$response = $graphQLServer->execute(
  $query,
  $variables,
);
 
// Print the response
echo $response->getContent();

要执行 GraphQL query,在终端中运行(使用 jq 美化 JSON 输出):

php retrieve-github-artifacts.php | jq

最后,要从 GraphQL 响应中提取构建产物 URL 并注入到 WP-CLI,运行:

GITHUB_ARTIFACT_URLS=$(php retrieve-github-artifacts.php \
  | grep -E -o '"spaceSeparatedArtifactDownloadURLs\":"(.*)"' \
  | cut -d':' -f2- | cut -d'"' -f2- | rev | cut -d'"' -f2- | rev \
  | sed 's/\\\//\//g')
wp plugin install ${GITHUB_ARTIFACT_URLS} --force --activate

如视频所示,我们已经能够在没有 WordPress 的情况下运行 Gato GraphQL。


订阅我们的新闻通讯

及时了解 Gato GraphQL 的所有更新。