十八、使用 CMS 和 GraphQL 创建 Nuxt 应用

在前面的章节中,您从头开始创建 API,以便它们与 Nuxt 应用一起工作。构建一个个性化的 API 可能是有益的和令人满意的,但它可能并不适合所有情况。自下而上构建 API 非常耗时。在本章中,我们将探讨第三方系统,这些系统可以为我们提供所需的 API 服务,而无需我们从头开始构建它们。理想情况下,我们希望使用一个能够帮助我们管理内容的系统——一个内容管理系统CMS

WordPress 和 Drupal 是流行的 CMSE。它们包含了值得研究的 API。在本书中,我们将使用WordPress。除了像 WordPress 这样的 CMSE,我们还将研究无头 CMSE。无头 CMS 就像 WordPress 一样,但它是一个没有前端演示的纯 API 服务,可以在 Nuxt 中完成,就像我们在本书中所做的那样。Keystone将是我们将在本书中探讨的无头 CMS。然而,WordPress API 和 Keystone API 是两种不同的 API。具体来说,前者是一个REST API,而后者是一个GraphQL API。但它们是什么?简而言之,REST API 是一种使用对GETPUTPOSTDELETE数据的 HTTP 请求的 API。在前几章中创建的 API 是 REST API。GraphQL 是实现 GraphQL 规范(技术标准)的 API。

GraphQLAPI 是 RESTAPI 的替代品。为了演示如何使用这两种不同类型的 API 提供相同的结果,我们将使用第 4 章中提供的示例 Nuxt 应用网站添加视图、路由。和过渡。这可以在本书的 GitHub 存储库的/chapter-4/nuxt-universal/sample-website/中找到。我们将重构现有页面(主页、关于、项目、内容和项目子页面),这些页面由文本和图像(特色图像、全屏图像和单个项目图像)组成。我们还将通过从 API 获取数据而不是硬编码来重构导航,就像我们在前几章中为其他 Nuxt 应用所做的那样。使用 CMS,我们可以通过 API 动态获取导航数据,而不管它是 REST 还是 GraphQLAPI。

此外,我们将使用这些 CMSE 生成静态 Nuxt 页面(您在第 14 章中了解了这些页面,使用 linter、格式化器和部署命令,以及第 15 章使用 Nuxt创建 SPA)。因此,在本章结束时,您将对本书中所学内容有一个完整和最终的了解。

在本章中,我们将介绍以下主题:

  • 在 WordPress 中创建无头 RESTAPI
  • 介绍 Keystone
  • 介绍 GraphQL
  • 集成 Keystone、GraphQL 和 Nuxt

让我们从研究 WordPressRESTAPI 开始。

在 WordPress 中创建无头 RESTAPI

WordPress(WordPress.org)是一个用于通用网站开发的开源 PHPCMS。它不是默认的“无头”;它与模板系统堆叠在一起。这意味着视图和数据是相互交织的。然而,自 2015 年(WordPress 4.4)以来,REST API 基础设施已集成到 WordPress core 中供开发人员使用,现在,如果您将/wp-json/附加到基于网站的 URL,则可以访问所有默认端点。您还可以扩展 WordPress REST API 并添加自己的自定义端点。因此,我们可以通过忽略视图轻松地将 WordPress 用作“无头”RESTAPI。在接下来的章节中,您将了解如何实现这一点。为了加快开发过程,我们将安装以下 WordPress 插件:

如果您不想使用这些插件和元框,您可以创建自己的插件和元框。请在查看如何创建自定义元框 https://developer.wordpress.org/plugins/metadata/custom-meta-boxes/ 。另外,在上查看如何开发定制插件 https://developer.wordpress.org/plugins/intro/

For more information about the WordPress REST API, please visit https://developer.wordpress.org/rest-api/.

要使用这些插件或您的插件开发和扩展 WordPress REST API,首先,您需要下载 WordPress 并在您的机器上安装该程序。我们将在下一节学习如何做到这一点。

安装 WordPress 并创建我们的第一页

我们可以通过以下几种方式安装和服务 WordPress:

在本书中,我们将使用内置 PHP 服务器,因为这是启动 WordPress 的最简单方法,并且如果需要的话,只要它在同一个端口上运行,将来就可以更轻松地移动它;例如,localhost:4000。那么,让我们来看看如何做到这一点:

  1. 创建一个目录(使其也可写),并在其中下载和解压缩 WordPress。您可以从下载 WordPresshttps://wordpress.org/ 。在您解压缩的 WordPress 目录中,您应该会看到一些带有/wp-admin//wp-content//wp-includes/目录的.php文件。
  2. 通过 PHP 管理员创建一个 MySQL 数据库(例如,nuxt-wordpress)。
  3. 导航到该目录并使用内置 PHP 为 WordPress 提供服务,如下所示:
$ php -S localhost:4000
  1. 将浏览器指向localhost:4000并使用所需的 MySQL 凭据(数据库名称、用户名和密码)和 WordPress 用户帐户信息(用户名、密码和电子邮件地址)安装 WordPress。
  2. 使用您在localhost:4000/wp-admin/上的用户凭证登录 WordPress 管理员界面,并在Pages标签下创建一些主页(主页、关于、项目、联系人)。
  3. 外观下导航到菜单,并通过在菜单名输入字段中添加menu-main来创建站点导航。
  4. 选择“添加菜单项”下显示的所有页面(联系人、关于、项目、主页),然后单击“添加到菜单”,将其作为导航项添加到menu-main。您可以拖动项目并对其排序,以便按以下顺序读取:主页、关于、项目、联系人。然后,单击保存菜单按钮。
  5. (可选)将 WordPress 永久链接从普通选项更改为永久链接设置下的自定义结构(例如,值为/%postname%/)。
  6. 下载我们前面提到的插件,并将它们解压缩到/plugins/目录中。这可以在/wp-content/目录中找到。然后,通过管理员界面激活它们。

如果您检查nuxt-wordpress数据库中的wp_options表,您会看到siteurlhome字段中成功记录了端口4000。因此,从现在起,只要在这个端口使用内置 PHP 服务器运行 WordPress 项目目录,您就可以将它移动到任何您喜欢的地方。

虽然我们在 WordPress 中有主页和导航的数据,但是我们仍然需要Projects页面的子页面的数据。我们可以将它们添加到Page标签上,然后将它们附加到Projects页面上。但这些页面将共享相同的内容类型(在 WordPress 中称为 post 类型)pagepost 类型。最好将它们组织在一个单独的职位类型中,以便更容易地管理它们。在下一节中,我们将了解如何在 WordPress 中创建自定义帖子类型。

For more details about the WordPress installation process, please visit https://wordpress.org/support/article/how-to-install-wordpress/.

在 WordPress 中创建自定义帖子类型

我们可以从任何 WordPress 主题中的functions.php文件在 WordPress 中创建自定义帖子类型。但是,由于我们不打算使用 WordPress 模板系统为我们的内容提供视图,我们可以从 WordPress 提供的默认主题扩展一个子主题。然后,我们可以在主题外观下激活子主题。我们将使用“219”主题来扩展子主题,然后在此基础上创建自定义帖子类型。让我们开始:

  1. /themes/目录中创建一个名为twentynineteen-child的目录,并创建一个包含以下内容的style.css文件:
// wp-content/themes/twentynineteen-child/style.css
/*
 Theme Name: Twenty Nineteen Child
 Template: twentynineteen
 Text Domain: twentynineteenchild
*/

@import url("../twentynineteen/style.css");

Theme NameTemplateText Domain是扩展主题所需的最低标题注释,然后导入其父级的style.css文件。这些标题注释必须放在文件的顶部。

If you want to include more header comments in this child theme, please visit https://developer.wordpress.org/themes/advanced-topics/child-themes/.

  1. /twentynineteen-child/目录中创建一个functions.php文件,并使用此格式和 WordPress’register_post_type功能创建自定义帖子类型,如下所示:
// wp-content/themes/twentynineteen-child/functions.php
function create_something () {
    register_post_type('<name>', <args>);
}
add_action('init', 'create_something');

因此,要添加我们的自定义帖子类型,只需使用project作为类型名称并提供一些参数:

// wp-content/themes/twentynineteen-child/functions.php
function create_project_post_type () {
    register_post_type('project', $args);
}
add_action('init', 'create_project_post_type');

我们可以将标签和希望支持的内容字段添加到自定义帖子类型 UI 中,如下所示:

$args = [
    'labels' => [
        'name' => __('Project (Pages)'),
        'singular_name' => __('Project'),
        'all_items' => 'All Projects'
    ],
    //...
    'supports' => ['title', 'editor', 'thumbnail', 'page-attributes'],
];

For more information about the register_post_type function, please visit https://developer.wordpress.org/reference/functions/register_post_type/.

For more information about the custom post type UI, please visit https://wordpress.org/plugins/custom-post-type-ui/.

  1. (可选)我们还可以为该自定义帖子类型添加对categorytag的支持,如下所示:
'taxonomies' => [
    'category',
    'post_tag'
],

但是,这些是全局类别和标记实例,这意味着它们与其他帖子类型共享,例如PagePost帖子类型。因此,如果您只想为Project帖子类型指定特定类别,请使用以下代码:

// wp-content/themes/twentynineteen-child/functions.php
add_action('init', 'create_project_categories');
function create_project_categories() {
    $args = [
        'label' => __('Categories'),
        'has_archive' => true,
        'hierarchical' => true,
        'rewrite' => [
            'slug' => 'project',
            'with_front' => false
        ],
    ];
    $postTypes = ['project'];
    $taxonomy = 'project-category';
    register_taxonomy($taxonomy, $postTypes, $args);
}

For more information about registering taxonomies, please visit https://developer.wordpress.org/reference/functions/register_taxonomy/.

  1. (可选)如果您发现难以使用,则最好完全禁用所有帖子类型的 Gutenberg:
// wp-content/themes/twentynineteen-child/functions.php
add_filter('use_block_editor_for_post', '__return_false', 10);
add_filter('use_block_editor_for_post_type', '__return_false', 10);
  1. 在 WordPress 管理界面中激活子主题,并开始向项目标签添加project类型的页面。

您会注意到,可用于向项目页面添加内容的内容字段(titleeditorthumbnailpage-attributes)非常有限。我们需要更具体的内容字段,例如用于添加多个项目图像和全屏图像的内容字段。这与我们在home页面上遇到的问题相同,因为我们需要另一个内容字段,以便我们也可以添加多张幻灯片图像。要添加更多这些内容字段,我们需要自定义元框。您可以使用 ACF 插件或创建自己的自定义元框,并将其包含在functions.php文件中,或将其创建为插件。或者,您可以使用其他不同的元盒插件,如元盒(https://metabox.io/ 。这完全取决于你。

创建自定义内容字段并将所需内容添加到每个项目页面后,可以为项目页面、主页和导航扩展 WordPress REST API。我们将在下一节学习如何做到这一点。

扩展 WordPressRESTAPI

WordPress REST API 可以通过/wp-json/访问,并且是附加到基于站点的 URL 的入口路径。例如,您可以通过将浏览器指向localhost:4000/wp-json/来查看所有其他可用路由。您将看到每个路由中都有哪些端点可用,因为它们可以是 GET 端点或 POST 端点。例如,/wp-json/wp/v2/pages路由有一个用于列出页面的 GET 端点和一个用于创建页面的 POST 端点。您可以在上找到关于这些默认路由和端点的更多信息 https://developer.wordpress.org/rest-api/reference/

但是,如果您有自定义文章类型和自定义内容字段,则需要自定义路由和端点。我们可以通过在functions.php文件中注册register_rest_route函数来创建这些文件的自定义版本,如下所示:

add_action('rest_api_init', function () { , and then followed by the available endpoint
    $args = [
        'methods' => 'GET',
        'callback' => '<do_something>',
    ];
    register_rest_route(<namespace>, <route>, $args);
});

让我们学习如何扩展 WordPress REST API:

  1. 创建用于获取导航和单个页面的全局命名空间和端点:
// wp-content/themes/twentynineteen-child/functions.php
$namespace = 'api/v1/';

add_action('rest_api_init', function () use ($namespace) {
    $route = 'menu';
    $args = [
        'methods' => 'GET',
        'callback' => 'fetch_menu',
    ];
    register_rest_route($namespace, $route, $args);
});

add_action('rest_api_init', function () use ($namespace) {
    $route = 'page/(?P<slug>[a-zA-Z0-9-]+)';
    $args = [
        'methods' => 'GET',
        'callback' => 'fetch_page',
    ];
    register_rest_route($namespace, $route, $args);
});

注意,我们在匿名函数中使用 PHPuse关键字将全局名称空间传递给add_action的每个块。有关 PHPuse关键字和匿名函数的更多信息,请访问https://www.php.net/manual/en/functions.anonymous.php

For more information about the register_rest_route function from WordPress, please visit https://developer.wordpress.org/reference/functions/register_rest_route/.

  1. 创建端点以获取单个项目页面并列出项目页面:
// wp-content/themes/twentynineteen-child/functions.php
add_action('rest_api_init', function () use ($namespace) {
    $route = 'project/(?P<slug>[a-zA-Z0-9-]+)';
    $args = [
        'methods' => 'GET',
        'callback' => 'fetch_project',
    ];
    register_rest_route($namespace, $route, $args);
});

add_action('rest_api_init', function () use ($namespace) {
    $route = 'projects/(?P<page_number>\d+)';
    $args = [
        'methods' => 'GET',
        'callback' => 'fetch_projects',
    ];
    register_rest_route($namespace, $route, $args);
});
  1. 创建获取menu-main导航项的fetch_menu函数:
// wp-content/themes/twentynineteen-child/functions.php
function fetch_menu ($data) {
    $menu_items = wp_get_nav_menu_items('menu-main');

    if (empty($menu_items)) {
        return [];
    }

    return $menu_items;
}

我们使用 WordPress 的wp_get_nav_menu_items功能来帮助我们获取导航。

For more information about the wp_get_nav_menu_items function, please visit https://developer.wordpress.org/reference/functions/wp_get_nav_menu_items/.

  1. 创建一个fetch_page函数,通过 slug(或 path)获取页面:
// wp-content/themes/twentynineteen-child/functions.php
function fetch_page ($data) {
    $post = get_page_by_path($data['slug'], OBJECT, 'page');

    if (!count((array)$post)) {
        return [];
    }
    $post->slides = get_field('slide_items', $post->ID);

    return $post;
}

在这里,我们使用 WordPress 的get_page_by_path函数获取页面。有关此功能的更多信息,请访问https://developer.wordpress.org/reference/functions/get_page_by_path/

我们还使用 ACF 插件中的get_field函数来获取附在页面上的幻灯片图像列表,然后将它们作为slides推送到$post对象。有关此功能的更多信息,请访问https://www.advancedcustomfields.com/resources/get_field/

  1. 创建fetch_project函数以获取单个项目页面:
// wp-content/themes/twentynineteen-child/functions.php
function fetch_project ($data) {
    $post = get_page_by_path($data['slug'], OBJECT, 'project');

    if (!count((array)$post)) {
        return [];
    }
    $post->fullscreen = get_field('full_screen_image', $post->ID);
    $post->images = get_field('image_items', $post->ID);

    return $post;
}

同样,我们使用 WordPressget_page_by_path函数为我们获取页面,使用 ACFget_field函数获取附在项目页面上的图像(全屏图像和项目图像),然后将它们推送到$post对象,如fullscreenimages

  1. 创建一个fetch_projects函数,用于获取项目页面列表,每页 6 项:
// wp-content/themes/twentynineteen-child/functions.php
function fetch_projects ($data) {
    $paged = $data['page_number'] ? $data['page_number'] : 1;
    $posts_per_page = 6;
    $post_type = 'project';
    $args = [
        'post_type' => $post_type,
        'post_status' => ['publish'],
        'posts_per_page' => $posts_per_page,
        'paged' => $paged,
        'orderby' => 'date'
    ];
    $posts = get_posts($args);

    if (empty($posts)) {
        return [];
    }

    foreach ($posts as &$post) {
        $post->featured_image = get_the_post_thumbnail_url($post->ID);
    }
    return $posts;
}

在这里,我们使用 WordPress 的get_posts函数和必需的参数来获取列表。有关此功能的更多信息,请访问https://developer.wordpress.org/reference/functions/get_posts/

然后,我们循环每个项目页面,并将它们的特色图片从 WordPress 推送到get_the_post_thumbnail_url函数中。有关此功能的更多信息,请访问https://developer.wordpress.org/reference/functions/get_the_post_thumbnail_url/

  1. 我们还需要计算数据(上一个页码和下一个页码),以便对项目页面进行分页,因此,不只是返回$posts,而是将其与分页数据一起返回为以下数组中的items
$total = wp_count_posts($post_type);
$total_max_pages = ceil($total->publish / $posts_per_page);

return [
    'items' => $posts,
    'total_pages' => $total_max_pages,
    'current_page' => (int)$paged,
    'next_page' => (int)$paged === (int)$total_max_pages ? null :
     $paged + 1,
    'prev_page' => (int) $paged === 1 ? null : $paged - 1,
];

在这里,我们使用wp_count_posts函数来统计发布的项目页面总数。有关此功能的更多信息,请访问https://developer.wordpress.org/reference/functions/wp_count_posts/

  1. 登录 WordPress 管理员界面,进入工具下的重写规则,点击刷新规则按钮刷新 WordPress 重写规则。
  2. 转到浏览器,测试刚刚创建的自定义 API 管线:
/wp-json/api/v1/menu
/wp-json/api/v1/page/<slug>
/wp-json/api/v1/projects/<number>
/wp-json/api/v1/project/<slug>

您应该会在浏览器屏幕上看到一堆 JSON 原始数据。JSON 原始数据可能很难读取,但您可以使用 JSON 验证器JSONLint,在处漂亮地打印数据 https://jsonlint.com/ 。或者,您也可以使用Firefox,它可以漂亮地打印您的数据。

You can find the entire code for this in /chapter-18/cross-domain/backend/wordpress/, in this book's GitHub repository. You can find a sample database (nuxt-wordpress.sql) in it too. The default username and password in this sample database for logging into the WordPress admin UI is admin.

做得好!您已成功扩展 WordPress REST API,使其支持自定义帖子类型。我们不需要在 WordPress 中开发任何新主题来查看我们的内容,因为这将由 Nuxt 处理。我们可以保留 WordPress 的现有主题以预览内容。这意味着我们只使用 WordPress 远程托管我们的网站内容,包括所有媒体文件(图像、视频等)。此外,我们可以使用 Nuxt 生成静态页面(就像我们在前几章中所做的那样),并将所有媒体文件从 WordPress 流式传输到我们的 Nuxt 项目,以便我们可以在本地托管它们。我们将在下一节学习如何做到这一点。

与 WordPress 的 Nuxt 和流媒体图像集成

将 Nuxt 与 WordPress REST API 集成类似于您在前几章中学习并创建的跨域 API 集成。然而,在本节中,我们将改进用于加载图像的插件,要求它们来 img/目录。但是,由于我们的图像上传到 WordPress CMS,并保存在 WordPress 项目的/uploads/目录中,因此我们需要重构我们的资产加载插件,以便 img/目录中找到图像时,它需要这些图像;否则,我们只是从 WordPress 远程加载它们。让我们开始:

  1. 在 Nuxt 配置文件中为 Axios 实例设置remote URL,如下所示:
// nuxt.config.js
const protocol = 'http'
const host = process.env.NODE_ENV === 'production' ? 'your-domain.com' : 'localhost'
const ports = {
  local: '3000',
  remote: '4000'
}
const remoteUrl = protocol + '://' + host + ':' + ports.remote

module.exports = {
  env: {
    remoteUrl: remoteUrl,
  }
}
  1. 创建一个 Axios 实例,并将其直接作为$axios注入 Nuxt 上下文。另外,使用inject函数将此 Axios 实例添加到app选项的上下文中:
// plugins/axios.js
import axios from 'axios'

let baseURL = process.env.remoteUrl
const api = axios.create({ baseURL })

export default (ctx, inject) => {
  ctx.$axios = api
  inject('axios', api)
}
  1. 重构 asset loader 插件,如下所示:
// plugins/utils.js
import Vue from 'vue'

Vue.prototype.$loadAssetImage = src => {
  var array = src.split('/')
  var last = [...array].pop()
  if (process.server && process.env.streamRemoteResource === true) {
    var { streamResource } = require(img/js/stream-resource')
    streamResource(src, last)
    return
  }

  try {
    return require(img/img/' + last)
  } catch (e) {
    return src
  }
}

在这里,我们将图像 URL 字符串拆分为一个数组,从数组中的最后一项获取图像的文件名(例如,my-image.jpg,并将其存储在last变量中。然后,我们需要使用文件名(last在本地创建图像。如果抛出错误,这意味着图像不存在 img/目录中,因此我们只返回图像的 URL(src`)。

但是,当我们的应用在服务器端运行且streamRemoteResource选项为true时,我们将使用streamResource功能将图像从远程 URL 流式传输 img/目录。在接下来的步骤中,您将了解如何创建此选项(就像remoteURL`选项一样)。

  1. img/目录中创建一个具有streamResource功能的stream-resource.js`文件,如下所示:
// img/js/stream-resource.js
import axios from 'axios'
import fs from 'fs'

export const streamResource = async (src, last) => {
  const file = fs.createWriteStream(img/img/' + last)
  const { data } = await axios({
    url: src,
    method: 'GET',
    responseType: 'stream'
  })
  data.pipe(file)
}

在这个函数中,我们使用普通 Axios 通过指定stream作为响应类型来请求远程资源的数据。然后,我们使用 Node.js 内置文件系统(fs)包中的createWriteStream函数和必要的文件路径 img/`目录中创建映像。

For more information about the fs package and its createWriteStream function, please visit https://nodejs.org/api/fs.html and https://nodejs.org/api/fs.htmlfs_fs_createwritestream_path_options.

For more information about the Node.js stream's pipe event in the response data and the Node.js stream itself, please visit https://nodejs.org/api/stream.htmlstream_event_pipe and https://nodejs.org/api/stream.htmlstream_stream.

  1. 在 Nuxt 配置文件中注册两个插件:
// nuxt.config.js
plugins: [
  '~/plugins/axios.js',
  '~/plugins/utils.js',
],
  1. 重构/pages/目录中主页的index.vue以使用这两个插件,如下所示:
// pages/index.vue
async asyncData ({ error, $axios }) {
  let { data } = await $axios.get('/wp-json/api/v1/page/home')
  return {
    post: data
  }
}

<template v-for="slide in post.slides">
  <img :src="$loadAssetImage(slide.image.sizes.medium_large)">
</template>

在这里,我们使用插件中的$axios请求 WordPress API。收到数据后,我们将其填充到<template>块中。$loadAssetImage函数用于为我们运行有关如何加载和处理图像的逻辑。

/pages/目录中的其余页面应进行重构,并遵循与主页相同的模式。它们是/about.vue/contact.vue/projects/index.vue/projects/_slug.vue/projects/pages/_number.vue。另外,您需要对/components/目录中的组件执行此操作;也就是说,/projects/project-items.vue。您可以在本节末尾提供的 GitHub 存储库中找到这些已完成文件的存储库路径。

  1. 使用自定义环境变量NUXT_ENV_GEN创建另一个脚本命令,并将stream作为其值放入 Nuxt 项目的package.json文件中:
// package.json
"scripts": {
  "generate": "nuxt generate",
  "stream": "NUXT_ENV_GEN=stream nuxt generate"
}

在 Nuxt 中,如果在package.json文件中创建一个前缀为NUXT_ENV_的环境变量,它将自动注入 Node.js 进程环境。完成此操作后,您可以通过process.env对象在整个应用中访问它–包括您可能在 Nuxt 配置文件的env属性中设置的其他自定义属性

For more information about the env property in Nuxt, please visit https://nuxtjs.org/api/configuration-env/.

  1. 在 Nuxt 配置文件的env属性中为资产加载器插件(我们在步骤 3中重构)定义streamRemoteResource选项,如下所示:
// nuxt.config.js
env: {
  streamRemoteResource: process.env.NUXT_ENV_GEN === 'stream' ? 
   true : false
},

当我们从NUXT_ENV_GEN环境变量中获取stream值时,该streamRemoteResource选项将被设置为true;否则,它总是设置为false。因此,当此选项设置为true时,asset loader 插件将开始为我们将远程资源流式传输 img/`目录。

  1. (可选)如果 Nuxt 爬虫由于未知原因无法检测到动态路由,则在 Nuxt 配置文件的generate选项中手动生成这些路由,如下所示:
// nuxt.config.js 
import axios from 'axios'
export default {
  generate: {
    routes: async function () {
      const projects = await axios.get(remoteUrl + '/wp-json/api/v1/projects')
      const routesProjects = projects.data.map((project) => {
        return {
          route: '/projects/' + project.post_name,
          payload: project
        }
      })

      let totalMaxPages = Math.ceil(routesProjects.length / 6)
      let pagesProjects = []
      Array(totalMaxPages).fill().map((item, index) => {
        pagesProjects.push({
          route: '/projects/pages/' + (index + 1),
          payload: null
        })
      })

      const routes = [ ...routesProjects, ...pagesProjects ]
      return routes
    }
  }
}

在这个可选步骤中,我们使用 Axios 获取属于projectspost 类型的所有子页面,并使用 JavaScriptmap方法循环这些页面以生成它们的路由。然后,我们计算子页面的长度,通过将子页面除以六(每页制作六个项目),计算出最大页数(totalMaxPages)。之后,我们使用 JavaScriptArray对象将totalMaxPages号转换成一个数组,然后使用 JavaScriptfillmappush方法循环数组,以生成分页的动态路由。最后,我们使用 JavaScript spread 操作符连接子页面和分页的路由,然后将它们作为 Nuxt 的单个数组返回,以便为我们生成动态路由。

For more information about the JavaScript map, fill, and push methods, please visit https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill, and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push, respectively.

  1. 首先运行stream命令,然后在终端上运行generate命令,如下所示:
$ npm run stream && npm run generate

我们使用stream命令通过生成第一批静态页面将远程资源流式传输 img/目录,然后使用generate命令重新生成静态页面。此时,webpack 将处 img/目录中的图像,并将其与静态页面一起导出到/dist/文件夹中。因此,在运行这两个命令之后,您应该看到远程资源 img//dist/`中被流式传输和处理。您可以导航到这两个目录并检查下载的资源。

You can find the Nuxt app of this section in /chapter-18/cross-domain/frontend/nuxt-universal/nuxt-wordpress/axios-vanilla/ in this book's GitHub repository.

做得好!您已经成功地将 Nuxt 与 WordPress REST API 集成,并为静态页面传输远程资源。WordPress 可能不是每个人的选择,因为它不符合PHP 标准建议PSRs)https://www.php-fig.org/ )并且有自己的做事方式。但它是在 2003 年 PSR 和许多现代 PHP 框架发布之前发布的。从那时起,它已经能够支持无数的企业和个人。当然,它已经发展并为编辑和开发人员提供了一个最为用户友好的管理 UI。

如果这还不能说服您使用 WordPress 作为 API,那么还有其他选择。在下一节中,我们将研究 REST API 的替代品——GraphQL API——以及 Node.js 中 WordPress 的替代品——Keystone。Keystone 使用 GraphQL 交付其 API。在深入研究 GraphQL 之前,我们将了解 Keystone 并学习如何开发定制的 CMS。

介绍 Keystone

Keystone 是一个可扩展的无头 CMS,用于在 Node.js 中构建 GraphQL API。它是开源的,并配备了一个非常体面的管理用户界面,您可以在其中管理您的内容。与 WordPress 一样,您可以在名为列表的 Keystone 中创建自定义内容类型,然后通过 GraphQLAPI 查询您的内容。您可以从源代码创建列表,就像创建 RESTAPI 一样。您可以为 API 添加所需的内容,使其具有高度的可伸缩性和可扩展性。

要使用 Keystone,首先需要准备一个用于存储内容的数据库。Keystone 支持 MongoDB 和 PostgreSQL。您需要安装和配置其中一个,然后找到 Keystone 的连接字符串。您在第 9 章中了解了 MongoDB,添加了服务器端数据库,因此再次将其用作 Keystone 的数据库对您来说应该不是问题。但是 PostgreSQL 呢?让我们看看。

For more information about Keystone, please visit https://www.keystonejs.com/.

安装和保护 PostgreSQL(Ubuntu)

PostgreSQL,也称为 Postgres,是一个对象关系数据库系统,通常与 MySQL 相比,MySQL 是一个(纯)关系数据库管理系统(RDBMS)。两者都是开源的,都使用表,但也有不同之处。

例如,Postgres 基本上是 SQL 兼容的,而 MySQL 是部分兼容的,MySQL 在读取速度方面执行得更快,而 PostgreSQL 在注入复杂查询方面执行得更快。有关博士后的更多信息,请访问https://www.postgresql.org/

您可以在许多不同的操作系统上安装 Postgres,包括 Linux、macOS 和 Windows。根据您的操作系统,您可以按照的官方指南进行操作 https://www.postgresql.org/download/ 将其安装到您的机器上。我们将通过以下步骤向您展示如何在 Linux(特别是 Ubuntu)上安装和保护它:

  1. 使用 Ubuntu 的apt打包系统更新本地包索引并从 Ubuntu 的默认存储库安装 Postgres:
$ sudo apt update
$ sudo apt install postgresql postgresql-contrib
  1. 通过检查 Postgres 的版本来验证它:
$ psql -v

如果您获得以下输出,则表示您已成功安装:

/usr/lib/postgresql/12/bin/psql: option requires an argument -- 'v'
Try "psql --help" for more information.

路径中的数字12表示您的机器上有 Postgres 版本 12。

  1. 从终端输入 Postgres shell:
$ sudo -u postgres psql

您应该在终端上获得与以下类似的输出:

postgres@lau-desktop:~$ psql
psql (12.2 (Ubuntu 12.2-2.pgdg19.10+1))
Type "help" for help.

postgres=
  1. 列出使用 Postgres\du命令的默认用户:
postgres= \du

您应该获得两个默认用户,如下所示:

Role name 
-----------
postgres 
root

我们将使用终端上的交互式提示向列表中添加新的管理用户(或角色)。但是,我们需要首先退出 Postgres shell:

postgres= \q
  1. 使用--interactive标志键入以下命令:
$ sudo -u postgres createuser --interactive

关于新角色的名称以及该角色是否应具有超级用户权限,您应该看到以下两个问题:

Enter name of role to add: user1
Shall the new role be a superuser? (y/n) y

在这里,我们称新用户为user1。它拥有超级用户权限,就像默认用户一样。

  1. 使用sudo -u postgres psql登录 Postgres shell,使用\du命令验证新用户。您应该看到它已添加到列表中。
  2. 使用以下 SQL 查询向新用户添加密码:
ALTER USER user1 PASSWORD 'password';

如果您获得以下输出,则表示您已成功为此用户添加密码:

ALTER ROLE
  1. 退出 Postgres shell。现在,您可以使用 PHP 的管理员(https://www.adminer.org/ )使用此用户登录 Postgres,然后在那里添加一个新数据库,该数据库将在以后安装 Keystone 时需要。然后,您可以对刚刚创建的数据库的 Postgres 连接字符串使用以下格式:
postgres://<username>:<password>@localhost/<dbname>

请注意,出于安全原因,任何用户都需要密码才能从 Adminer 登录到数据库。因此,在数据库中添加安全性是一种很好的做法,尤其是在生产环境中,无论它是 MySQL、Postgres 还是 MongoDB 数据库。MongoDB 呢?在前面的章节中,您学习了如何安装和使用它,但它还没有得到保护。我们将在下一节中了解如何做到这一点。

安装和保护 MongoDB(Ubuntu)

现在,您应该知道如何安装 MongoDB 了。因此,在本节中,我们将重点讨论 MongoDB 中数据库的安全性。为了保护 MongoDB,我们将首先向 MongoDB 添加一个管理用户,如下所示:

  1. 从终端连接到 Mongo 外壳:
$ mongo
  1. 选择admin数据库,并向该数据库添加一个新的用户名和密码(例如 root 和 password)用户,如下所示:
> use admin
> db.createUser(
  {
    user: "root",
    pwd: "password",
    roles: [ { role: "userAdminAnyDatabase", db: "admin" }, 
     "readWriteAnyDatabase" ]
  }
)
  1. 退出 shell 并从终端打开 MongoDB 配置文件:
$ sudo nano /etc/mongod.conf
  1. 查找security部分,删除散列,添加authorization设置,如下图:
// mongodb.conf
security:
  authorization: "enabled"
  1. 保存并退出文件,然后重新启动 MongoDB:
$ sudo systemctl restart mongod
  1. 通过检查 MongoDB 的状态来验证配置:
$ sudo systemctl status mongod

如果您看到"active"状态,则表示您已正确配置。

  1. 使用密码和--authenticationDatabase选项以"root"身份登录。另外,提供存储用户的数据库的名称,在本例中为"admin"
$ mongo --port 27017 -u "root" -p "password" --authenticationDatabase "admin"
  1. 创建新数据库(例如,test)并将新用户附加到该数据库:
> use test
db.createUser(
  {
    user: "user1",
    pwd: "password",
    roles: [ { role: "readWrite", db: "test" } ]
  }
)
  1. user1登录退出并测试数据库:
$ mongo --port 27017 -u "user1" -p "password" --authenticationDatabase "test"
  1. 测试您是否可以访问此test数据库,但不能访问其他数据库:
> show dbs

如果未收到任何输出,则意味着您只有在身份验证后才有权访问此数据库。您可以使用以下格式为 KiStEngor 或任何其他应用(例如 Express、膝关节炎等)提供 MangoDB 连接字符串:

mogodb://<username>:<password>@localhost:27017/<dbname>

同样,向数据库添加安全性是一种很好的做法,特别是在生产环境中,但使用 MongoDB 开发应用时,如果不启用身份验证,则更容易、更快。您始终可以在本地开发中禁用它,而只需在生产服务器中启用它。

现在,两个数据库系统(Postgres 和 MongoDB)都准备好了,您可以选择其中一个来构建 Keystone 应用。那么,让我们开始吧!

安装和创建 Keystone 应用

启动 Keystone 项目有两种方法——从头开始,或者使用 Keystone 脚手架工具keystone-app。如果要从头开始,则需要手动安装任何与 Keystone 相关的软件包。其中包括构建应用所需的最低 Keystone 软件包和其他 Keystone 软件包。让我们来看看这个手册的安装:

  1. 创建项目目录并安装所需的最低软件包–Keystone 软件包本身、Keystone GraphQL 软件包(被视为 Keystone 中的应用)和数据库适配器:
$ npm i @keystonejs/keystone
$ npm i @keystonejs/app-graphql
$ npm i @keystonejs/adapter-mongoose
  1. 安装所需的其他 Keystone 软件包,例如 Keystone Admin UI 软件包(在 Keystone 中被视为应用)和用于注册列表的 Keystone field 软件包:
$ npm i @keystonejs/app-admin-ui
$ npm i @keystonejs/fields
  1. 在您的根目录中创建一个空的index.js文件,并导入您刚刚安装的软件包:
// index.js
const { Keystone } = require('@keystonejs/keystone')
const { GraphQLApp } = require('@keystonejs/app-graphql')
const { AdminUIApp } = require('@keystonejs/app-admin-ui')
const { MongooseAdapter } = require('@keystonejs/adapter-mongoose')
const { Text } = require('@keystonejs/fields')
  1. 创建 Keystone 的新实例,并将数据库适配器的新实例传递给它,如下所示:
const keystone = new Keystone({
  name: 'My Keystone Project',
  adapter: new MongooseAdapter({ mongoUri: 'mongodb://localhost/your-
    db-name' }),
})

Check out the following guide to learn how to configure the Mongoose adapter: https://www.keystonejs.com/keystonejs/adapter-mongoose/. We will cover this again when we install Keystone with the scaffolding tool.

  1. 创建一个简单的列表–例如Page列表–并定义您需要的字段,以便存储此列表中每个项目的数据:
keystone.createList('Page', {
  fields: {
    name: { type: Text },
  },
})

将 GraphQL 的列表名称大写是一种惯例。我们很快就会谈到这一点。

  1. 导出keystone实例和应用以便执行:
module.exports = {
  keystone,
  apps: [new GraphQLApp(), new AdminUIApp()]
}
  1. 创建一个package.json文件(如果您还没有这样做),并将以下keystone命令添加到脚本中,如下所示:
"scripts": {
  "dev": "keystone"
}
  1. 通过在终端上运行dev脚本启动应用:
$ npm run dev

您应该在终端上看到以下输出。这意味着您已成功启动应用:

 Command: keystone dev
✓ Validated project entry file ./index.js
✓ Keystone server listening on port 3000
✓ Initialised Keystone instance
✓ Connected to database
✓ Keystone instance is ready at http://localhost:3000
∞ Keystone Admin UI: http://localhost:3000/admin
∞ GraphQL Playground: http://localhost:3000/admin/graphiql
∞ GraphQL API: http://localhost:3000/admin/api

做得好!您已经启动并运行了第一个也是最简单的 Keystone 应用。在这个应用中,你在localhost:3000/admin/api有一个 GraphQL API,在localhost:3000/admin/graphiql有一个 GraphQL 游乐场,在localhost:3000/admin有一个 Keystone 管理员 UI。但是我们如何使用 GraphQLAPI 和 GraphQLAYER 呢?请放心,我们将在下一节中讨论这一点。

启动一个新的 Keystone 应用一点都不难,是吗?您只需要安装 Keystone 需要的和您需要的。然而,启动 Keystone 应用最简单的方法是使用脚手架工具。使用 scaffolding 工具的好处是,它在安装过程中附带了一些 Keystone 应用的可选示例,它们可以作为指南和模板非常有用。这些可选样本如下所示:

  • 启动器本例演示了使用 Keystone 进行基本用户身份验证。 *Todo此示例演示了一个简单的应用,用于将项目添加到Todo列表,以及一些前端集成(HTML、CSS 和 JavaScript)。 *空白此示例提供了一个基本的起点,以及 Keystone 管理 UI、GraphQL API 和 GraphQL 游乐场。这些与手动安装中的一样,但没有 Keystonefield包。 *Nuxt此示例演示了与 Nuxt.js 的简单集成。**

**我们将选择blank**选项,因为它为我们提供了所需的基本包,因此我们可以在这些包之上构建我们的列表。让我们来看一看:

  1. 使用终端上的任意名称创建一个新的 Keystone 应用:
$ npm init keystone-app <app-name>
  1. 回答 Keystone 提出的问题,如下所示:
✓ What is your project name?
✓ Select a starter project: Starter / Blank / Todo / Nuxt
✓ Select a database type: MongoDB / Postgre
  1. 安装完成后,进入项目目录:
$ cd <app-name>
  1. 如果您使用的是安全 Postgres,那么只需提供连接字符串以及 Keystone 的用户名、密码和数据库:
// index.js
const adapterConfig = { knexOptions: { connection: 'postgres://
 <username>:<password>@localhost/<dbname>' } }

请注意,如果未启用身份验证,则只需从字符串中删除<username>:<password>@。然后,运行以下命令安装数据库表:

$ npm run create-tables

For more information about the Knex database adapter, please visit https://www.keystonejs.com/quick-start/adapters or visit knex.js at http://knexjs.org/. It is a query builder for PostgreSQL, MySQL, and SQLite3.

  1. 如果您使用的是安全 MongoDB,那么只需提供连接字符串以及 Keystone 的用户名、密码和数据库:
// index.js
const adapterConfig = { mongoUri: 'mogodb://<username>:<password>@localhost:27017/<dbname>' }

请注意,如果未启用身份验证,则只需从字符串中删除<username>:<password>@

For more information about the Mongoose database adapter, please visit https://www.keystonejs.com/keystonejs/adapter-mongoose/ or visit Mongoose at https://mongoosejs.com/. MongoDB is a schemaless database system by nature, so this adapter is used as a schema solution to model the data in our app.

  1. 将服务器默认端口从3000更改为4000以服务 Keystone 应用。您只需在dev脚本中添加PORT=4000即可,如下所示:
// package.json
"scripts": {
  "dev": "cross-env NODE_ENV=development PORT=4000 ...",
}

我们将 Keystone 的端口更改为4000的原因是我们正在为 Nuxt 应用保留端口3000

  1. 在我们的项目中安装nodemon。这将允许我们监控 Keystone 应用中的更改,以便它可以为我们重新加载服务器:
$ npm i nodemon --save-dev
  1. 安装此包后,将nodemon --exec命令添加到dev脚本中,如下所示:
// package.json
"scripts": {
  "dev": "... nodemon --exec keystone dev",
}

For more information about nodemon, please visit https://nodemon.io/.

  1. 使用以下命令启动 Keystone 应用的开发服务器:
$ npm run dev

您应该在终端上看到以下输出。这意味着您已成功安装 Keystone 应用:

✓ Keystone instance is ready at http://localhost:4000
∞ Keystone Admin UI: http://localhost:4000/admin
∞ GraphQL Playground: http://localhost:4000/admin/graphiql
∞ GraphQL API: http://localhost:4000/admin/api

这与执行手动安装相同,但在不同的端口上。在这个应用中,你在localhost:4000/admin/api有一个 GraphQL API,在localhost:4000/admin/graphiql有一个 GraphQL 游乐场,在localhost:4000/admin有一个 Keystone 管理员 UI。在我们可以使用 GraphQLAPI 和 GraphQLplayerd 做任何事情之前,我们必须向 Keystone 应用添加列表,并开始从 Keystone 管理 UI 注入数据。我们将在下一节开始向应用添加列表和字段。

You can find the apps we created from both of these installation techniques in /chapter-18/keystone/ in this book's GitHub repository.

创建列表和字段

在 Keystone 中,列表是模式。模式是具有描述数据类型的数据模型。在 Keystone 中也是如此:列表模式由字段组成,这些字段具有描述它们接受的数据的类型,就像我们在手动安装中所做的一样,在手动安装中,我们有一个Page列表,其中包含一个name字段和一个Text类型。

Keystone 中有许多不同的字段类型,例如FileFloatCheckboxContentDateTimeSlugRelationships。您可以在的文档中找到所需的其他 Keystone 字段类型 https://www.keystonejs.com/

要将字段及其类型添加到列表中,只需安装在项目目录中保存这些字段类型的 Keystone 包。例如,@keystonejs/fields包包含CheckboxTextFloatDateTime字段类型等。您可以在上找到关于其余字段类型的信息 https://www.keystonejs.com/keystonejs/fields/fields 。在安装了所需的字段类型包之后,您只需导入它们,并通过使用 JavaScript 解构分配创建列表来解包所需的字段类型。

然而,列表可能会随着时间的推移而增长,这意味着它们可能会变得杂乱无章,难以跟上。因此,最好在/list/目录中的单独的文件中创建列表,以便更好地维护,如下所示:

// lists/Page.js
const { Text } = require('@keystonejs/fields')

module.exports = {
  fields: {...},
}

然后,您只需将其导入到index.js文件中。那么,让我们看看构建 Keystone 应用需要哪些模式/列表和其他 Keystone 包。我们将创建的列表如下:

  • 用于存储主页面的Page模式/列表,如homeaboutcontactprojects
  • 用于存储项目页面的Project模式/列表
  • 一个Image模式/列表,用于存储主页面和项目页面的图像
  • 仅用于存储主页图像的Slide Image模式/列表
  • 用于存储站点链接的Nav Link模式/列表

我们将用于创建这些列表的 Keystone 包如下所示:

****现在,让我们安装并使用它们创建列表:

  1. 通过 npm 安装我们前面提到的 Keystone 软件包:
$ npm i @keystonejs/app-static
$ npm i @keystonejs/file-adapters
$ npm i @keystonejs/fields-wysiwyg-tinymce
  1. @keystonejs/app-static导入index.js并定义要保存静态文件的路径和文件夹名称:
// index.js
const { StaticApp } = require('@keystonejs/app-static');

module.exports = {
  apps: [
    new StaticApp({
      path: '/public',
      src: 'public'
    }),
  ],
}
  1. /lists/目录中创建一个File.js文件。然后,使用@keystonejs/fields中的FileText@keystonejs/file-adapters中的Slug字段类型为Image列表定义字段。这将允许您将文件上载到本地位置;即/public/files/
// lists/File.js
const { File, Text, Slug } = require('@keystonejs/fields')
const { LocalFileAdapter } = require('@keystonejs/file-adapters')

const fileAdapter = new LocalFileAdapter({
  src: './public/files',
  path: '/public/files',
})

module.exports = {
  fields: {
    title: { type: Text, isRequired: true },
    alt: { type: Text },
    caption: { type: Text, isMultiline: true },
    name: { type: Slug },
    file: { type: File, adapter: fileAdapter, isRequired: true },
  }
}

在前面的代码中,我们定义了一个字段列表(titlealtcaptionnamefile),以便存储每个上传文件的元信息。在每个列表模式中都有name字段是一种很好的做法,这样我们可以在该字段中存储一个唯一的名称,作为 Keystone 管理 UI 中的标签。我们可以使用它轻松地识别每个注入的列表项。要为该字段生成唯一名称,我们可以使用Slug类型,默认情况下,该类型从title字段生成唯一名称。

For more information about the field types that we used in the preceding code, please visit the following links:

有关LocalFileAdapter的更多信息,请访问https://www.keystonejs.com/keystonejs/file-adapters/localfileadapter

我们的应用文件可以使用CloudinaryFileAdapter上传到 Cloudinary。

For more information about how to set up an account so that you can host files on Cloudinary, please visit https://cloudinary.com/.

  1. /lists/目录中创建一个SlideImage.js文件,并用一个额外的字段类型Relationship定义与File.js文件中相同的字段,以便您可以将幻灯片图像链接到项目页面:
// lists/SlideImage.js
const { Relationship } = require('@keystonejs/fields')

module.exports = {
  fields: {
    // ...
    link: { type: Relationship, ref: 'Project' },
  },
}

For more information about the Relationship field, please visit https://www.keystonejs.com/keystonejs/fields/src/types/relationship/.

  1. /lists/目录中创建Page.js文件,并使用@keystonejs/fields@keystonejs/fields-wysiwyg-tinymce中的TextRelationshipSlugWysiwyg字段类型定义Page列表的字段,如下所示:
// lists/Page.js
const { Text, Relationship, Slug } = require('@keystonejs/fields')
const { Wysiwyg } = require('@keystonejs/fields-wysiwyg-tinymce')

module.exports = {
  fields: {
    title: { type: Text, isRequired: true },
    excerpt: { type: Text, isMultiline: true },
    content: { type: Wysiwyg },
    name: { type: Slug },
    featuredImage: { type: Relationship, ref: 'Image' },
    slideImages: { type: Relationship, ref: 'SlideImage', many:
     true },
  },
}

在前面的代码中,我们定义了一个字段列表(titleexcerptcontentnamefeaturedImage,slideImages),这样我们就可以存储我们将注入到该内容类型中的每个主页的数据。请注意,我们将featuredImage链接到Image列表,并将slideImages链接到SlideImage列表。我们希望允许在slideImages字段中放置多个图像,因此我们将many选项设置为true

For more information about these one-to-many and many-to-many relationships, please visit https://www.keystonejs.com/guides/new-schema-cheatsheet.

  1. /lists/目录中创建一个Project.js文件,为Project列表定义与File.js文件中相同的字段,增加两个字段(fullscreenImageprojectImages
// lists/Project.js
const { Text, Relationship, Slug } = require('@keystonejs/fields')
const { Wysiwyg } = require('@keystonejs/fields-wysiwyg-tinymce')

module.exports = {
  fields: {
    //...
    fullscreenImage: { type: Relationship, ref: 'Image' },
    projectImages: { type: Relationship, ref: 'Image', many:
     true },
  },
}
  1. /lists/目录中创建NavLink.js文件,并使用@keystonejs/fields中的TextRelationshipSlugInteger字段类型为NavLink列表定义字段(titleordernamelinksubLinks,如下所示:
// lists/NavLink.js
const { Text, Relationship, Slug, Integer } = require('@keystonejs/fields')

module.exports = {
  fields: {
    title: { type: Text, isRequired: true },
    order: { type: Integer, isRequired: true },
    name: { type: Slug },
    link: { type: Relationship, ref: 'Page' },
    subLinks: { type: Relationship, ref: 'Project', many: true },
  },
}

这里,我们使用order字段按照链接项在 GraphQL 查询中的数字位置对其进行排序。你很快就会知道的。subLinks字段是一个示例,演示了如何在 Keystone 中创建简单的子链接。因此,我们可以通过将项目页面附加到此字段向主链接添加多个子链接,该字段使用Relationship字段类型链接到Project列表。

For more information about the Integer field type, please visit https://www.keystonejs.com/keystonejs/fields/src/types/integer/.

  1. /lists/目录导入文件,并从中开始创建列表架构,如下所示:
// index.js
const PageSchema = require('./lists/Page.js')
const ProjectSchema = require('./lists/Project.js')
const FileSchema = require('./lists/File.js')
const SlideImageSchema = require('./lists/SlideImage.js')
const NavLinkSchema = require('./lists/NavLink.js')

const keystone = new Keystone({ ... })

keystone.createList('Page', PageSchema)
keystone.createList('Project', ProjectSchema)
keystone.createList('Image', FileSchema)
keystone.createList('SlideImage', SlideImageSchema)
keystone.createList('NavLink', NavLinkSchema)
  1. 通过在终端上运行dev脚本启动应用:
$ npm run dev

您应该在终端上看到一个 URL 列表,该列表与上一节中显示的相同。这意味着您已在localhost:4000上成功启动应用。因此,现在,您可以将浏览器指向localhost:4000/admin并开始从 Keystone 管理 UI 注入内容和上载文件。一旦准备好了内容和数据,就可以使用 GraphQL API 和 GraphQL 查询它们。但在此之前,您应该了解什么是 GraphQL,以及如何独立于 Keystone 创建和使用它。那么,让我们来看看吧!

You can find the source code for this app in /chapter-18/cross-domain/backend/keystone/ in this book's GitHub repository.

介绍 GraphQL

GraphQL 是一种开源查询语言、服务器端运行时(执行引擎)和规范(技术标准)。但这意味着什么?这是怎么一回事?GraphQL 是一种查询语言,它是 GraphQL 的“QL”部分的代表。具体来说,它是一种客户端查询语言。但是,这又意味着什么呢?以下示例将解决您对 GraphQL 查询的任何疑问:

{
 planet(name: "earth") {
   id
   age
   population
 }
}

与前一个类似的 GraphQL 查询在 HTTP 客户端(如 Nuxt 或 Vue)中用于将查询发送到服务器,以换取 JSON 响应,如下所示:

{
  "data": {
    "planet": {
      "id": 3,
      "age": "4543000000",
      "population": "7594000000"
    }
  }
}

如您所见,您获得了所请求的字段(agepopulation的具体数据,仅此而已。这就是 GraphQL 与众不同的地方,并赋予客户机请求他们想要的东西的能力。很酷,很刺激,不是吗?但返回 GraphQL 响应的服务器中是什么?GraphQLAPI 服务器(服务器端运行时)。

GraphQL 查询由客户端通过 HTTP 端点通过POST方法以字符串形式发送到 GraphQL API 服务器。服务器提取并处理查询字符串。然后,与任何典型的 API 服务器一样,GraphQLAPI 将从数据库或其他服务/API 获取数据,并以 JSON 响应的形式将其返回给客户机。

那么,我们可以使用像 Express 这样的服务器作为 GraphQLAPI 服务器吗?是和否。所有合格的 GraphQL 服务器都必须实现两个核心组件,如 GraphQL 规范中指定的,它们验证、处理然后返回数据:模式和解析器。

GraphQL 模式是类型定义的集合,由客户端可以请求的对象和对象具有的字段组成。另一方面,GraphQL 解析器是附加到字段的函数,这些字段在客户端进行查询或变异时返回值。例如,以下是查找行星的类型定义:

type Planet {
  id: Int
  name: String
  age: String
  population: String
}

type Query {
  planet(name: String): Planet
}

在这里,您可以看到 GraphQL 使用了强类型模式——每个字段都必须定义为标量类型(可以是整型、布尔型或字符串的单个值)或对象类型。PlanetQuery类型是对象类型,StringInt是标量类型。必须使用函数解析对象类型中的每个字段,如下所示:

Planet: {
  id: (root, args, context, info) => root.id,
  name: (root, args, context, info) => root.name,
  age: (root, args, context, info) => root.age,
  population: (root, args, context, info) => root.population,
}

Query: {
  planet: (root, args, context, info) => {
    return planets.find(planet => planet.name === args.name)
  },
}

前面的示例是用 JavaScript 编写的,但是 GraphQL 服务器可以用任何编程语言编写,只要您遵循并实现中 GraphQL 规范中概述的内容 https://spec.graphql.org/ 。以下是不同语言中 GraphQL 实现的一些示例:

只要符合 GraphQL 规范,您就可以自由创建新的实现,但在本书中我们只使用 GraphQL.js。现在,您可能有一些更深层次的问题–查询类型到底是什么?我们知道它是object类型,但为什么我们需要它?我们需要在模式中包含它吗?简而言之,答案是肯定的。

在下一节中,我们将更详细地了解这一点,并找出为什么需要它。我们还将了解如何使用 Express 作为 GraphQLAPI 服务器。所以,继续阅读。

理解 GraphQL 模式和解析器

上一节讨论的用于查找行星的示例模式和解析器假定我们使用 GraphQL 模式语言,这有助于我们创建 GraphQL 服务器所需的 GraphQL 模式。我们可以使用 Node.js 包 GraphQL Tools 中的makeExecutableSchema函数从 GraphQL 模式语言轻松创建 GraphQL.jsGraphQLSchema实例。

You can find out more information about this package at https://www.graphql-tools.com/ or https://github.com/ardatan/graphql-tools.

GraphQL 模式语言是一种“快捷方式”——用于构建 GraphQL 模式及其类型系统的简写符号。在使用这种简写符号之前,我们应该先看看如何从实现 GraphQL 规范的 GraphQL.js 中的低级对象和函数(如GraphQLObjectTypeGraphQLStringGraphQLList等)构建 GraphQL 模式。让我们安装这些软件包并使用 Express 创建一个简单的 GraphQL API 服务器:

  1. 通过 npm 安装 Express、GraphQL.js 和 GraphQL HTTP 服务器中间件:
$ npm i express
$ npm i express-graphql
$ npm i graphql

GraphQL HTTP 服务器中间件是一种中间件,它允许我们使用任何 HTTP web 框架创建 GraphQL HTTP 服务器,该框架实现 Connect 支持中间件的方式,如 Express、Restify 和 Connect 本身。

For more information about these packages, please visit the following links:

// index.js
const express = require('express')
const graphqlHTTP = require('express-graphql')
const graphql = require('graphql')

const app = express()
const port = process.env.PORT || 4000
  1. 使用行星列表创建虚拟数据:
// index.js
const planets = [
  { id: 3, name: "earth", age: 4543000000, population:
    7594000000 },
  { id: 4, name: "mars", age: 4603000000, population: 0 },
]
  1. 定义Planet对象类型和客户端可以查询的字段:
// index.js
const planetType = new graphql.GraphQLObjectType({
  name: 'Planet',
  fields: {
  id: { ... },
  name: { ... },
  age: { ... },
  population: { ... },
})

请注意,对于 GraphQL 模式的创建,name字段中的对象类型大写是一种惯例。

  1. 定义各种类型以及如何解析每个字段的值:
// index.js
id: {
  type: graphql.GraphQLInt,
  resolve: (root, orgs, context, info) => root.id,
},
name: {
  type: graphql.GraphQLString,
  resolve: (root, orgs, context, info) => root.name,
},
age: {
  type: graphql.GraphQLString,
  resolve: (root, orgs, context, info) => root.age,
},
population: {
  type: graphql.GraphQLString,
  resolve: (root, orgs, context, info) => root.population,
},

请注意,每个解析器函数都接受以下四个参数:

  • root:从父对象类型解析的对象或值(在步骤 6中的查询)。
  • args:字段设置后可以接收的参数。参见步骤 8
  • context:一个可变 JavaScript 对象,它保存所有解析程序共享的顶级数据。在我们使用 Express 时,默认情况下是 Node.js HTTP 请求对象(IncomingMessage。我们可以修改这个上下文对象并添加想要共享的常规数据,例如身份验证和数据库连接。参见步骤 10
  • info:一个 JavaScript 对象,它保存关于当前字段的信息,如字段名、返回类型、父类型(在本例中为Planet)以及通用模式详细信息。

如果解析当前字段的值不需要它们,则可以省略它们。

  1. 定义Query对象类型和客户端可以查询的字段:
// index.js
const queryType = new graphql.GraphQLObjectType({
  name: 'Query',
  fields: {
    hello: { ... },
    planet: { ... },
  },
})
  1. 定义类型并解析您希望如何返回hello字段的值:
// index.js
hello: {
  type: graphql.GraphQLString,
  resolve: (root, args, context, info) => 'world',
}
  1. 定义类型并解析您希望如何返回planet字段的值:
// index.js
planet: {
  type: planetType,
  args: {
    name: { type: graphql.GraphQLString }
  },
  resolve: (root, args, context, info) => {
    return planets.find(planet => planet.name === args.name)
  },
}

请注意,我们将创建并存储在planetType变量中的Planet对象类型传递给Query对象类型中的planet字段,以便建立它们之间的关系。

  1. 构建一个 GraphQL 模式实例,其中包含所需的query字段和您刚才用其中的字段、类型、参数和解析程序定义的Query对象类型,如下所示:
// index.js
const schema = new graphql.GraphQLSchema({ query: queryType })

请注意,query键必须作为 GraphQL 查询根类型提供,以便我们的查询可以链接到Planet对象类型中的字段。我们可以说,Planet对象类型是Query对象类型(根类型)的子类型或子类型,必须使用planet字段中的type字段在父对象(Query中建立它们的关系。

  1. 使用 GraphQL HTTP 服务器中间件作为带有 GraphQL 架构实例的中间件,在 Express 允许的名为/graphiql的端点上建立 GraphQL 服务器,如下所示:
// index.js
app.use(
  '/graphiql',
  graphqlHTTP({ schema, graphiql: true }),
)

建议将graphiql选项设置为true,以便在浏览器上加载 GraphQL 端点时可以使用 GraphQL IDE。

在这个顶层,您还可以使用graphqlHTTP中间件中的context选项修改 GraphQL API 的上下文,如下所示:

context: {
  something: 'something to be shared',
}

通过执行此操作,您可以从任何解析器访问此顶级数据。这可能非常有用。很酷,不是吗?

  1. 最后,在加载完所有数据后,在您的终端上使用index.js文件中的以下行的node index.js命令启动服务器:
// index.js
app.listen(port)
  1. 将浏览器指向localhost:4000/graphiql。您应该看到 graphqlide,这是一个可以测试 graphqlapi 的 UI。因此,在左侧的输入区域中键入以下查询:
// localhost:4000/graphiql
{
  hello
  planet (name: "earth") {
    id
    age
    population
  }
}

您应该看到,当您点击 play 按钮时,前面的 GraphQL 查询已与右侧的 JSON 对象交换:

// localhost:4000/graphiql
{
  "data": {
    "hello": "world",
    "planet": {
      "id": 3,
      "age": "4543000000",
      "population": "7594000000"
    }
  }
}

做得很好–您已经使用低级方法使用 Express 创建了一个基本的 GraphQLAPI 服务器!我们希望这能让您全面了解如何使用 GraphQL 模式和解析器创建 GraphQLAPI 服务器。我们还希望您能在 GraphQL 中看到这两个核心组件之间的关系,并且我们已经回答了您的问题;也就是说,Query类型到底是什么?我们为什么需要它?我们需要在模式中包含它吗?答案是肯定的,查询(对象)类型是在创建 GraphQL 模式时必须提供的根对象类型(通常称为根Query类型)。

但是您可能仍然有一些问题和抱怨,特别是关于解析程序的问题-您肯定会发现在步骤 5中为Planet对象类型中的字段定义解析程序既乏味又愚蠢,因为它们除了返回从查询对象解析的值之外什么都不做。有没有办法避免这种痛苦的重复?答案是肯定的:您没有为模式中的每个字段指定它们,这取决于默认解析器。但我们如何做到这一点?我们将在下一节中找到答案。

You can find this and other examples in /chapter-18/graphql-api/graphql-express/ in this book's GitHub repository.

理解 GraphQL 默认解析器

如果未为字段指定解析程序,默认情况下,此字段将采用已由父对象解析的对象中的属性值,也就是说,如果该对象的属性名称与字段名称匹配。因此,Planet对象类型中的字段可以重构如下:

fields: {
  id: { type: graphql.GraphQLInt },
  name: { type: graphql.GraphQLString },
  age: { type: graphql.GraphQLString },
  population: { type: graphql.GraphQLString },
}

这些字段的值将返回到对象中的属性,该属性已由引擎盖下的父级(查询类型)解析,如下所示:

root.id
root.name
root.age
root.population

因此,换句话说,当为字段显式指定冲突解决程序时,将始终使用该冲突解决程序,即使父级的冲突解决程序为该字段返回任何值。例如,让我们为Planet对象类型中的id字段显式指定一个值,如下所示:

fields: {
  id: {
    type: graphql.GraphQLInt,
    resolve: (root, orgs, context, info) => 2,
  },
}

我们已经知道地球和火星的默认 ID 值是 3 和 4,它们由Query对象类型(父对象)解析,如前一节步骤 8所示。但是这些解析的值永远不会被使用,因为它们被 ID 的解析程序中的值覆盖。那么,让我们查询地球或火星,如下所示:

{
  planet (name: "mars") {
    id
  }
}

在这种情况下,JSON 响应中总是会出现2

{
  "data": {
    "planet": {
      "id": 2
    }
  }
}

这很聪明,不是吗?它使我们免于痛苦的重复——也就是说,如果对象类型中有大量字段的话。然而,到目前为止,我们一直在使用 GraphQL.js 以最痛苦的方式构建模式。这是因为我们希望看到并理解如何从低级类型创建 GraphQL 模式。我们可能不想在现实生活中走这条漫长而曲折的道路,尤其是在一个大型项目中。相反,我们应该更喜欢使用 GraphQL 模式语言为我们构建模式和解析器。在下一节中,我们将向您展示如何使用 GraphQL 模式语言和Apollo server轻松创建 GraphQL API 服务器,作为 GraphQL HTTP 服务器中间件的替代方案。所以,继续读下去!

使用 Apollo 服务器创建 GraphQL API

Apollo 服务器是 Apollo 平台为构建 GraphQLAPI 而开发的一种开源且符合 GraphQL 规范的服务器。我们可以使用它独立或与其他 No.js Web 框架,如 Express、膝关节炎、HAPI 等。我们将像本书中一样使用 Apollo 服务器,但如果您想将其用于其他框架,请访问https://github.com/apollographql/apollo-serverinstallation-integrations

在这个 GraphQLAPI 中,我们将创建一个服务器,用于按标题和作者查询书籍集合。让我们开始:

  1. 通过 npm 安装 Apollo Server 和 GraphQL.js 作为项目依赖项:
$ npm i apollo-server
$ npm i graphql
  1. 在项目根目录中创建一个index.js文件,并从apollo-server包中导入ApolloServergql函数:
// index.js
const { ApolloServer, gql } = require('apollo-server')

gql函数用于解析 GraphQL 操作和模式语言,方法是使用模板文字标记(或标记的模板文字)对它们进行包装。有关模板文字和标记模板的更多信息,请访问https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals

  1. 创建以下静态数据,其中包含作者和帖子列表:
// index.js
const authors = [
  { id: 1, name: 'author A' },
  { id: 2, name: 'author B' },
]

const posts = [
  { id: 1, title: 'Post 1', authorId: 1 },
  { id: 2, title: 'Post 2', authorId: 1 },
  { id: 3, title: 'Post 3', authorId: 2 },
]
  1. 定义AuthorPostQuery对象类型,以及客户端可以查询的字段:
// index.js
const typeDefs = gql`
  type Author {
   id: Int
   name: String
  }

  type Post {
   id: Int
   title: String
   author: Author
  }

  type Query {
    posts: [Post]
  }
`

注意,我们可以将AuthorPostQuery对象类型简写为Author类型、Post类型和Query类型。这比用“对象类型”来描述它们更清楚,因为它们就是这样。记住,Query类型除了本质上是一种对象类型外,也是 GraphQL 模式创建中的根类型。

注意我们如何建立AuthorPostPostQuery的关系,author字段的类型是Author类型。Author类型的字段有简单的标量类型(idname),而Post类型的字段有简单的标量类型(idtitle)和Author类型(author)。Query类型的唯一字段为Post类型,即posts,但它是一个帖子列表,因此我们必须使用类型修饰符将Post类型用开括号和闭括号括起来,以表示该posts字段将解析为一个Post对象数组。

For more information about the type modifier, please visit https://graphql.org/learn/schema/lists-and-non-null.

  1. 定义解析程序以指定如何解析Query类型中的posts字段和Post类型中的author字段的值:
// index.js
const resolvers = {
  Query: {
    posts: (root, args, context, info) => posts
  },

  Post: {
    author: root => authors.find(author => author.id === 
     root.authorId)
  },
}

请注意 GraphQL 模式语言如何帮助我们将解析程序与对象类型解耦,并且它们只在单个 JavaScript 对象中定义。JavaScript 对象中的解析器与对象类型“神奇地”连接,只要解析器的属性名映射类型定义中的字段名。因此,这个 JavaScript 对象称为解析器映射。在定义冲突解决程序之前,我们还必须在冲突解决程序映射中定义顶级属性名称(QueryPost),以便它们与类型定义中的对象类型(AuthorPostQuery)相匹配。但是我们不需要在此冲突解决程序映射中为Author类型定义任何特定的冲突解决程序,因为Author中的字段(idname的值由默认冲突解决程序自动解析。

需要注意的另一点是,Post类型中的字段(idtitle的值也由默认值解析。如果您不喜欢使用属性名来定义解析器,则可以使用解析器函数,只要函数名与类型定义中的字段名相对应。例如,author字段的解析器可以重写如下:

Post: {
  author (root) {
    return authors.find(author => author.id === root.authorId)
  },
}
  1. 使用类型定义和解析器从ApolloServer构造一个 GraphQL 模式实例。然后,启动服务器,如下所示:
// index.js
const server = new ApolloServer({ typeDefs, resolvers })

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`)
})
  1. 在终端上使用node命令启动 GraphQL API:
$ node index.js
  1. 将浏览器指向localhost:4000。您应该可以在屏幕上看到加载的 GraphQL 游乐场。从那里,您可以测试 GraphQLAPI。因此,在左侧的输入区域中键入以下查询:
{
  posts {
    title
    author {
      name
    }
  }
}

您应该看到,当您点击 play 按钮时,前面的 GraphQL 查询已与右侧的 JSON 对象交换:

{
  "data": {
    "posts": [
      {
        "title": "Post 1",
        "author": {
          "name": "author A"
        }
      },
      ...
    ]
  }
}

这太美了,太棒了,不是吗?这就是使用 GraphQL 模式语言和 Apollo 服务器构建 GraphQLAPI 的容易程度。在采用速记方法之前,了解如何创建 GraphQL 模式和解析器的漫长而痛苦的过程是值得的。一旦掌握了这些基本的具体知识,就应该能够轻松地查询使用 Keystone 存储的数据。在本书中,我们只介绍了 GraphQL 的一些类型,包括标量类型、对象类型、查询类型和类型修饰符。您还应该检查一些其他类型,例如突变类型、枚举类型、联合和输入类型以及接口。请在查看 https://graphql.org/learn/schema/

If you want to learn more about GraphQL, please visit https://graphql.org/learn/. For more information about Apollo Server, visit https://www.apollographql.com/docs/apollo-server/.

You can find the code that was used in this section, along with other example GraphQL type definitions, in /chapter-18/graphql-api/graphql-apollo/ in this book's GitHub repository.

现在,让我们学习如何使用 Keystone GraphQL API。

使用 Keystone GraphQL API

Keystone GraphQL API 的 GraphQL 游乐场位于localhost:4000/admin/graphiql。在这里,我们可以测试通过位于localhost:4000/admin的 Keystone 管理 UI 创建的列表。Keystone 将为创建的每个列表自动生成四个顶级 GraphQL 查询。例如,对于我们在上一节中创建的page列表,我们将得到以下查询:

  • allPages

此查询可用于获取Page列表中的所有项目。我们还可以搜索、限制和过滤结果,如下所示:

{
  allPages (orderBy: "name_DESC", skip: 0, first: 6) {
    title
    content
  }
}
  • _allPagesMeta

此查询可用于获取有关Page列表中项目的所有元信息,例如所有匹配项目的总计数,这对于分页非常有用。我们还可以搜索、限制和过滤结果,如下所示:

{
  _allPagesMeta (search: "a") {
    count
  }
}
  • Page

此查询可用于从Page列表中获取单个项目。我们只能使用id键的where参数来获取页面,如下所示:

{
  Page (where: { id: $id }) {
    title
    content
  }
}
  • _PagesMeta

此查询可用于获取关于Page列表本身的元信息,如名称、访问、模式和字段,如下所示:

{
  _PagesMeta {
    name
    access {
      read
    }
    schema {
      queries
      fields {
        name
      }
    }
  }
}

正如您所看到的,这四个查询以及过滤器、限制和排序参数为我们提供了足够的能力来获取我们需要的特定数据,仅此而已。更重要的是,在 GraphQL 中,我们可以通过一个单个请求获取多个资源,如下所示:

{
  _allPagesMeta {
    count
  },
  allPages (orderBy: "name_DESC", skip: 0, first: 6) {
    title
    content
  }
}

这太棒了,太有趣了,不是吗?在 RESTAPI 中,您可能必须为多个资源向多个 API 端点发送多个请求。GraphQL 为我们提供了另一种解决 RESTAPI 这一臭名昭著的问题的方法,该问题一直困扰着前端和后端开发人员。请注意,这四个顶级查询也适用于我们创建的其他列表,包括ProjectImageNavLink

For more information about these four top-level queries and the filter, limit, and sorting parameters, as well as the GraphQL mutations and execution steps, which are not covered in this book, please visit https://www.keystonejs.com/guides/intro-to-graphql/.

If you want to learn about how to query a GraphQL server in general, please visit https://graphql.org/learn/queries/.

现在,您已经掌握了 GraphQL 的基本知识,并且了解了 Keystone 的顶级 GraphQL 查询,现在是学习如何在 Nuxt 应用中使用它们的时候了。

集成 Keystone、GraphQL 和 Nuxt

Keystone 的 GraphQL API 端点位于localhost:4000/admin/api。与 RESTAPI(通常有多个端点)不同,GraphQLAPI 通常有一个端点用于所有查询。因此,我们将使用该端点从 Nuxt 应用发送 GraphQL 查询。最好先在 GraphQL 平台上测试我们的查询,以确认我们得到了所需的结果,然后在前端应用中使用这些经过测试的查询。此外,我们应该在前端应用的查询中始终使用query关键字从 GraphQLAPI 获取数据。

在本练习中,我们将重构为 WordPressAPI 构建的 Nuxt 应用。我们将查看/pages/index.vue/pages/projects/index.vue/pages/projects/_slug.vue/store/index.js文件。我们仍将使用 Axios 来帮助发送 GraphQL 查询。让我们来看看如何获得 CopQL 查询和 Axios 一起工作:

  1. 创建一个用于存储 GraphQL 查询的变量,以获取主页标题和我们附加到主页上的幻灯片图像:
// pages/index.vue
const GET_PAGE = `
  query {
    allPages (search: "home") {
      title
      slideImages {
        alt
        link {
          name
        }
        file {
          publicUrl
        }
      }
    }
  }
`

我们只需要图像将链接到的项目页面中的 slug,因此,name字段是我们将查询的唯一字段。我们只需要图像的相对公共 URL,publicUrl字段是我们想要从图像文件对象获得的唯一字段。此外,我们使用allPages查询而不是Page,因为通过其 slug 更容易获取页面,在本例中,slug 就是home

  1. 从 Axios 使用post方法将查询发送到 GraphQL API 端点:
// pages/index.vue
export default {
  async asyncData ({ $axios }) {
    let { data } = await $axios.post('/admin/api', {
      query: GET_PAGE
    })
    return {
      post: data.data.allPages[0]
    }
  },
}

请注意,我们只需要 GraphQLAPI 返回的数据中数组的第一项,因此我们使用0来定位第一项。

请注意,我们还应该按照重构此主页的相同模式重构/pages/about.vue/pages/contact.vue/pages/projects/index.vue/pages/projects/pages/_number.vue。您可以在本节末尾找到本书的 GitHub 存储库的路径,其中包含完整的代码。

  1. 创建一个变量,用于存储查询并允许您从端点获取多个资源,如下所示:
// components/projects/project-items.vue
const GET_PROJECTS = `
  query {
    _allProjectsMeta {
      count
    }
    allProjects (orderBy: "name_DESC", skip: ${ skip }, first: ${ 
     postsPerPage }) {
      name
      title
      excerpt
      featuredImage {
        alt
        file {
          publicUrl
        }
      }
    }
  }
`

如您所见,我们通过_allProjectsMeta获取项目页面总数,并通过orderByskipfirst过滤器通过allProjects获取项目页面列表。skipfirst过滤器的数据将作为变量传入;即分别为skippostsPerPage

  1. 根据路由参数计算skip变量的数据,将6设置为postsPerPage变量,然后使用 Axios 的post方法将查询发送到 GraphQL API 端点:
// components/projects/project-items.vue
data () {
  return {
    posts: [],
    totalPages: null,
    currentPage: null,
    nextPage: null,
    prevPage: null,
  }
},

async fetch () {
  const postsPerPage = 6
  const number = this.$route.params.number
  const pageNumber = number === undefined ? 1 : Math.abs(
    parseInt(number))
  const skip = number === undefined ? 0 : (pageNumber - 1) 
   * postsPerPage

  const GET_PROJECTS = `... `

  let { data } = await $axios.post('/admin/api', {
    query: GET_PROJECTS
  })

  //... continued in step 5.
}

如您所见,我们根据路由参数计算pageNumber数据,在fetch方法中,我们只能通过this.$route.params访问。skip数据是从pageNumberpostsPerPage计算出来的,然后我们将其传递给 GraphQL 查询并获取数据。在这里,我们将为/projects/projects/pages/1路线上的pageNumber0获取1,为/projects/pages/2路线上的pageNumber获取2,为skip获取6等等。此外,我们必须确保路由中的任何有意负面数据(例如,/projects/pages/-100)将通过使用 JavaScriptMath.abs函数变为正面。

For more information about the JavaScript Math.abs function, please visit https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs.

  1. 从服务器返回的count字段创建分页(下一页和上一页),然后像往常一样返回<template>块的数据,如下所示:
// components/projects/project-items.vue
let totalPosts = data.data._allProjectsMeta.count
let totalMaxPages = Math.ceil(totalPosts / postsPerPage)

this.posts = data.data.allProjects
this.totalPages = totalMaxPages
this.currentPage = pageNumber
this.nextPage = pageNumber === totalMaxPages ? null : pageNumber + 1
this.prevPage = pageNumber === 1 ? null : pageNumber - 1
  1. 创建一个变量,用于存储 slug 从端点获取单个项目页面的查询,如下所示:
// pages/projects/_slug.vue
const GET_PAGE = `
  query {
    allProjects (search: "${ params.slug }") {
      title
      content
      excerpt
      fullscreenImage { ... }
      projectImages { ... }
    }
  }
`

在这里,我们使用search过滤器通过allProjects获取项目页面。search过滤器的数据将从params.slug参数传入。fullscreenImagefullscreenImage中查询的字段与featuredImage中的字段相同;您可以在步骤 3中找到它们。

  1. 从 Axios 使用post方法将查询发送到 GraphQL API 端点:
// pages/projects/_slug.vue
async asyncData ({ params, $axios }) {
  const GET_PAGE = `...`

  let { data: { data: result } } = await $axios.post('/admin/api', 
   {
    query: GET_PAGE
  })

  return {
    post: result.allProjects[0],
  }
}

请注意,您还可以对嵌套对象或数组进行分解,并为该值指定一个变量。在前面的代码中,我们指定了result作为变量,以存储 GraphQL 返回的data属性的值。

  1. 创建一个变量,该变量将存储用于使用orderBy过滤器从端点获取NavLinks列表的查询,如下所示:
// store/index.js
const GET_LINKS = `
  query {
    allNavLinks (orderBy: "order_ASC") {
      title
      link {
        name
      }
    }
  }
`
  1. 从 Axios 使用post方法将查询发送到 GraphQL API 端点,然后将数据提交到存储状态:
// store/index.js
async nuxtServerInit({ commit }, { $axios }) {
  const GET_LINKS = `...`
  let { data } = await $axios.post('/admin/api', {
    query: GET_LINKS
  })
  commit('setMenu', data.data.allNavLinks)
}
  1. (可选)就像中的步骤 9与 WordPress中的 Nuxt 和流媒体图像集成一样,如果 Nuxt 爬虫由于未知原因无法检测到动态路由,则在 Nuxt 配置文件的生成选项中手动生成这些路由,如下所示:
// nuxt.config.js
import axios from 'axios'

export default {
  generate: {
    routes: async function () {
      const GET_PROJECTS = `
        query {
          allProjects { name }
        }
      `
      const { data } = await axios.post(remoteUrl + '/admin/api', {
        query: GET_PROJECTS
      })
      const routesProjects = data.data.allProjects.map(project => {
        return {
          route: '/projects/' + project.name,
          payload: project
        }
      })

      let totalMaxPages = Math.ceil(routesProjects.length / 6)
      let pagesProjects = []
      Array(totalMaxPages).fill().map((item, index) => {
        pagesProjects.push({
          route: '/projects/pages/' + (index + 1),
          payload: null
        })
      })

      const routes = [ ...routesProjects, ...pagesProjects ]
      return routes
    }
  },
}

在这个可选步骤中,您可以看到我们使用相同的 JavaScript 内置对象和方法—Arraymapfillpush,就像在中集成 WordPress中的 Nuxt 和流媒体图像一样,为我们计算出子页面和分页的动态路由,然后将它们作为单个数组返回,以便 Nuxt 生成它们的动态路由。

  1. 为开发或生产运行以下脚本命令:
$ npm run dev
$ npm run build && npm run start
$ npm run stream && npm run generate

请记住,如果您希望生成静态页面并在同一位置托管图像,我们可以将远程图像流式传输 img/目录,以便 webpack 可以为我们处理这些图像。因此,如果您想这样做,那么就像我们之前所做的那样,首先运行npm run stream将远程图像流式传输到您的本地光盘,然后运行npm run generate`使用图像重新生成静态页面,然后再将其托管到某个地方。

You can find the code for this exercise in /chapter-18/cross-domain/frontend/nuxt-universal/nuxt-keystone in this book's GitHub repository.

Apart from using Axios, you can also use Nuxt Apollo module to send GraphQL queries to the server. For more information about this module and its usage, please visit https://github.com/nuxt-community/apollo-module.

做得好!您已经成功地将 Nuxt 与 Keystone GraphQL API 和用于静态页面的流式远程资源集成在一起,就像 WordPress REST API 一样。我们特别希望 Keystone 和 GraphQL 为您展示了另一个激动人心的 API 选项。您甚至可以进一步学习本章学到的 GraphQL 知识,并为 Nuxt 应用开发 GraphQL API。您还可以使用许多其他技术将 Nuxt 提升到下一个级别,就像我们在本书中介绍的一些技术一样。这本书经历了一段相当长的旅程。我们希望它对您的 web 开发有所帮助,并且您可以尽可能地利用从本书中学到的知识。现在,让我们总结一下您在本章学到的知识。

总结

在本章中,您成功地创建了自定义 post 类型和路由,以扩展 WordPress REST API,并将其与 Nuxt 集成,并从 WordPress 流式传输远程资源以生成静态页面。您还通过创建列表和字段从 Keystone 定制了 CMS。然后,您学习了如何使用 GraphQL.js 在较低级别创建 GraphQL API,以及如何使用 GraphQL 模式语言和 Apollo 服务器在较高级别创建 GraphQL API。现在您已经掌握了 GraphQL 的基础,可以使用 GraphQL 查询和 Axios 从 Nuxt 应用查询 Keystone GraphQL API。最后,同样重要的是,您可以将远程资源从 Keystone 项目流式传输到 Nuxt 项目,以生成静态页面。做得好!

这是一个非常漫长的旅程。您已经从了解 Nuxt 的目录结构开始,添加页面、路由、转换、组件、Vuex 存储、插件和模块,然后创建用户登录和 API 身份验证,编写端到端测试,以及创建 Nuxt SPA(静态页面)。您还将 Nuxt 与其他技术、工具和框架集成,包括 MongoDB、RejectDB、MySQL、PostgreSQL 和 GraphQL;膝关节炎,快车,梯形石和肩关节;PHP 和 PSR;ZURB 基金会和 CSS 较少;而且更漂亮,ESLint 和 StandardJS。

我们希望这是一次鼓舞人心的旅程,希望您在项目中尽可能采用 Nuxt,并进一步使自己和社区受益。继续编码,鼓舞人心,保持灵感。我们祝你一切顺利。

请注意,本书的最后一个应用示例可以在作者的网站上找到。这是一款完全由 Nuxt 的static目标和 GraphQL 制作的静态生成的 web 应用!请在浏览 https://lauthiamkok.net/****