集成 Keystone、GraphQL 和 Nuxt

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

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

  1. 创建一个变量来存储 GraphQL 查询,以便获取主页的标题和附加到它的幻灯片图像:

    // pages/index.vue
    const GET_PAGE = `
    query {
      allPages (search: "home") {
        title
        slideImages {
          alt
          link {
            name
          }
          file {
            publicUrl
          }
        }
      }
    }
    `
    javascript

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

  2. 使用 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]
        }
      },
    }
    javascript

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

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

  3. 创建一个变量来存储查询,并允许你从端点获取多个资源,如下所示:

    // 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
          }
        }
      }
    }
    `
    javascript

    正如你所见,我们通过 _allProjectsMeta 获取项目页面的总数,并通过带有 orderBy、skip 和 first 过滤器的 allProjects 获取项目页面列表。skip 和 first 过滤器的数据将作为变量传递;即分别为 skip 和 postsPerPage。

  4. 从路由参数计算 skip 变量的数据,将 postsPerPage 变量设置为 6,然后使用 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
      })
      //... 在步骤 5 中继续。
    }
    javascript

    正如你所见,我们从路由参数计算 pageNumber 数据,我们只能通过 fetch 方法中的 this.$route.params 访问它。在我们将 skip 数据传递给 GraphQL 查询并获取数据之前,会根据 pageNumber 和 postsPerPage 计算 skip 数据。在这里,在 /projects 或 /projects/pages/1 路由上,我们将获得 pageNumber 为 1,skip 为 0;在 /projects/pages/2 路由上,我们将获得 pageNumber 为 2,skip 为 6,依此类推。此外,我们必须确保路由中任何故意的负数据(例如,/projects/pages/-100)都将通过使用 JavaScript Math.abs 函数转换为正数。

    有关 JavaScript Math.abs 函数的更多信息,请访问 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs。

  5. 从服务器返回的 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
    javascript
  6. 创建一个变量来存储通过 slug 从端点获取单个项目页面的查询,如下所示:

    // pages/projects/_slug.vue
    const GET_PAGE = `
    query {
      allProjects (search: "${ params.slug }") {
        title
        content
        excerpt
        fullscreenImage { ... }
        projectImages { ... }
      }
    }
    `
    javascript

    在这里,我们通过带有 search 过滤器的 allProjects 获取项目页面。search 过滤器的数据将从 params.slug 参数传递进来。fullscreenImage 和 fullscreenImage 中我们将查询的字段与 featuredImage 中的字段相同;你可以在步骤 3 中找到它们。

  7. 使用 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],
      }
    }
    javascript

    请注意,你还可以解构嵌套的对象或数组,并将变量分配给该值。在上面的代码中,我们已将 result 分配为变量,以便存储 GraphQL 返回的 data 属性的值。

  8. 创建一个变量来存储通过带有 orderBy 过滤器的端点获取 NavLink 列表的查询,如下所示:

    // store/index.js
    const GET_LINKS = `
    query {
      allNavLinks (orderBy: "order_ASC") {
        title
        link {
          name
        }
      }
    }
    `
    javascript
  9. 使用 Axios 的 post 方法将查询发送到 GraphQL API 端点,然后将数据提交到 store 状态:

    // 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)
    }
    javascript
  10. (可选)就像“与 Nuxt 集成并从 WordPress 流式传输图像”部分中的步骤 9 一样,如果 Nuxt 爬虫由于某些未知原因无法检测到动态路由,则在 Nuxt 配置文件中的 generate 选项中手动生成这些路由,如下所示:

    // 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

    在这个可选步骤中,你可以看到我们使用了与“与 Nuxt 集成并从 WordPress 流式传输图像”部分相同的 JavaScript 内置对象和方法——Array、map、fill 和 push——来计算子页面的动态路由和分页,然后将它们作为单个数组返回给 Nuxt 以生成其动态路由。

  11. 运行以下脚本命令进行开发或生产:

    $ npm run dev
    $ npm run build && npm run start
    $ npm run stream && npm run generate
    bash

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

你可以在本书 GitHub 存储库的 /chapter-18/crossdomain/frontend/nuxt-universal/nuxt-keystone 中找到此练习的代码。

除了使用 Axios,你还可以使用 Nuxt Apollo 模块向服务器发送 GraphQL 查询。有关此模块及其用法的更多信息,请访问 https://github.com/nuxt-community/apollo-module。

做得好!你已成功将 Nuxt 与 Keystone GraphQL API 集成,并为静态页面流式传输了远程资源——就像你对 WordPress REST API 所做的那样。我们希望 Keystone 和特别是 GraphQL 向你展示了另一个令人兴奋的 API 选项。你甚至可以进一步利用你在本章中学到的 GraphQL 知识,为你的 Nuxt 应用程序开发 GraphQL API。你还可以使用许多其他技术将 Nuxt 提升到一个新的水平,就像我们在本书中引导你了解的一些技术一样。这本书的旅程相当漫长。我们希望它对你的 Web 开发有所帮助,并且你可以尽可能地利用你从本书中学到的知识。现在,让我们总结一下你在本章中学到的内容。