轻狂侠客
开发大学

Nuxt3:基于Strapi/GraphQL的BFF实战 | 青训营笔记

by Marlene, 2023-01-21


笔者在青训营的大项目开发中遇到了一些需求和坑,有所沉淀,特成此文,以作交流。

前言

本篇文章主要带领大家在Strapi、GraphQL的加持下完成Nuxt3的BFF数据流转。

众所周知,Nuxt3是非常实用的基于Vue的SSR框架。网上有很多基于Nextjs的BFF教程,却缺乏关于Nuxt3方面的BFF教程。笔者在自我尝试的过程中踩了很多坑,所以在此沉淀成文,以资后人。

Strapi是一款非常便捷的Headless CMS,非常适合搭配Nuxt3搭建一般的内容展示网站。使用Strapi,我们完全可以不用过多费心在后端接口开发上,只需注重数据库结构设计以及前端开发即可。

然而,在CMS和前端之间还存在一个API获取与处理的中间部分,为了进一步提升效率,我们使用GraphQL来进行API层面的处理。

安装

Strapi以及Nuxt安装教程较多,这里不再简述。

环境配置

因为后续我们会经常用到本地域名 和 CMS 域名,所以我们拿一个变量来存储它们,后续根据环境区分也很方便。
我们可以利用nuxt提供的runtimeConfig来定义和获取。

// nuxt.config.js

export default defineNuxtConfig({
  ...
  runtimeConfig: {
    public: {
      ...
      strapi_base_url: process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8886' : 'https://cms.xxx',
      graphql_url: process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8886/graphql' : 'https://cms.xxx/graphql',
    },
    ...
  },
  ...
})

之后我们就可以通过以下方式拿到:

const runtimeConfig = useRuntimeConfig()
const url = runtimeConfig.public.graphql_url

踩坑

笔者曾使用http://localhost:8886/作为baseURL,本身没有任何问题,但当其在服务端请求数据时发生了问题,无法请求,只有在客户端才能正常发出请求。因此,这里,笔者尝试使用了http://127.0.0.1/解决了这个问题。这个问题的原因,笔者暂时还不清楚,请大佬不吝赐教。

为什么使用GraphQL?

image.png
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。它是相对于Strapi默认的RESTful API 设计而言的。

  • 只获取你想要的数据
  • 多种数据,一次完成
  • 请求直观,方便开发
  • 服务解耦,节省沟通

在Strapi中使用GraphQL的优势


上图是使用Strapi获取部分数据的截图。可以看出有许多可以优化的地方:

  1. 请求参数populate=deep是每次请求都需要带上的,这样我们才可以请求更深一层的数据。
  2. 我们在这里需要的是data数据,不需要一些meta数据
  3. 响应结果的结构体有很多冗余项,例如无意义的iddataattributes字段,他们会增加很多TS类型的成本,不必要的调试成本
  4. 每个结构体都加上了 createdAt、 publishedAt、updatedAt 三个字段,实际上针对这个需求,我们是不需要这些字段的,随着接口层级的增加,过多不被使用的字段会增加我们接口的复杂度和可维护性

当我们使用上了GraphQL,上面的1、2、4点都能够解决。第3点,笔者专门在前端实现了一个类似于@strapi/transformer插件的功能,解决了这部分问题。

CMS安装GraphQL

yarn add @strapi/plugin-graphql

使用GraphQL

事实上,有很多基于Nuxt的graphql插件,甚至@nuxt/strapi插件也支持使用graphql。但笔者在实际尝试的过程中,不断遇到函数未定义,无法引用,Node版本过高等问题,踩了许多坑。后来,笔者尝试自己写一个方法实现获取数据的部分,发现实际代码量较少,所以我们在实际使用graphql的过程中,完全没必要使用插件等库即可自行优雅的开发。

为方便Nuxt全局使用,我们在/composables文件夹新建一个graphql.js文件。

GraphQL接口处理

实际上,通过调试我们可以发现一次graphql请求是一次post请求。当我们请求cms的/graphql地址,通过post方式发送一段data即可返回我们想要的数据结果。那么这段data的结构是什么样?

{
  query:``
}

接下来,原理清楚,编写代码即可。
笔者尝试使用Nuxt提供的useFetch等方法获取数据,但均显示undefined。尝试引用方法,但也无果。故,笔者使用axios库来获取graphql数据。详细代码如下:

import axios from 'axios'
export async function useGraphql(query) {
  const runtimeConfig = useRuntimeConfig() //获取cms地址,根据运行环境提供本地或部署网址
  const data = JSON.stringify({
    query,
  })
  const config = {
    method: 'post',
    url: runtimeConfig.public.graphql_url,
    headers: {
      'Content-Type': 'application/json',
      'Accept': '*/*',
      'Connection': 'keep-alive',
    },
    data,
  }

  const res = await axios(config)

  return res.data
}

接口优化

2.png

可以看到,我们请求得到的数据结构体非常冗余。“data”、“attributes”字段都是非必要的。那么我们需要将其去除。
原本,若是RESTful API, Strapi有Transformer插件,可以提供类似功能,但其不支持graphql。无奈,笔者尝试自行写出这部分功能。

仔细整理逻辑,我们可以想到以下思路:通过不断深层遍历,尝试将所有不必要的字段去除。

这里,我们通过递归的方法实现。递归有两个必要因素。一个是处理方法,一个是结束条件。

处理方法可以定义一个新函数。

const removeAttributeWrapper = (data) => {
  if ('attributes' in data) {
    const _ = data.attributes
    delete data.attributes
    return removeAttributeWrapper({ ...data, ..._ })
  }
  if ('data' in data) {
    const _ = data.data
    if (Array.isArray(_)) {
      return data // 不去掉data字段
    }
    else {
      delete data.data
      return removeAttributeWrapper({ ...data, ..._ })
    }
  }
  return data
}

函数遇到可遍历的object/array类型则继续遍历,所以结束条件是遇到非object/array类型。

有了结束方法和处理方法,我们就能实现我们的函数。

const removeStrapiWrapper = (data) => {
  if (Array.isArray(data)) {
    const _ = data.map((item) => {
      return removeStrapiWrapper(removeAttributeWrapper(item))
    })
    return _
  }
  else if (Object.prototype.toString.call(data) === '[object Object]') {
    const _ = removeAttributeWrapper(data)
    Object.entries(_).forEach(([k, v]) => {
      _[k] = removeStrapiWrapper(v)
    })
    return _
  }
  else {
    return data
  }
}

然后我们需要修改一下graphql请求的函数,将请求得到的结果去除不必要的字段。

export async function useGraphql(query) {
  const runtimeConfig = useRuntimeConfig()
  const data = JSON.stringify({
    query,
  })
  const config = {
    method: 'post',
    url: runtimeConfig.public.graphql_url,
    headers: {
      'Content-Type': 'application/json',
      'Accept': '*/*',
      'Connection': 'keep-alive',
    },
    data,
  }
  const res = await axios(config)
  return removeStrapiWrapper(res.data)
}

这样我们就可以得到精简的结构体了。

{
  "navs":{
    "data":[
      {
        "id": 1,
        "name": "首页",
        "url": "/",
        "tag": "null"
      },
      {
        "id": 2,
        "name": "沸点",
        "url": "/pins",
        "tag": "邀请有礼"
      }
    ]
  }
}

BFF接口定义

CMS 接口配置好了以后还不能直接在页面中调用,我们需要配置一层 BFF 层,即服务于前端的数据层。因为我们通常配置的数据是站在结构体的角度的,并不一定可以由前端调用,往往还需要复杂的数据处理,为了提高数据层的复用程度,我们增加 BFF 层,将 CMS 接口包一层,进行相关处理后,前端页面只调用我们定义的 BFF 层接口,不直接与 CMS 配置的接口产生交互。

BFF 的全称是「Backend For Frontend」,顾名思义就是面向前端的后端。它的主要职责就是针对页面的数据诉求,进行服务的调度以及数据的组装和适配。

Nuxt文件约定式路由

在定义接口前,我们得先来了解一下 Nuxt3 接口的路由是怎么配置的?

Nuxt路由分为动态路由、预定义路由、全捕获路由。我们这里使用动态路由的方式构建API即可。

server
| ——api
     | —— navs.js
     | —— ...

如此,我们便可以通过/api/navs获取navs.js里面方法返回的结果。
知道了 Api 路由的原理,下面来开发我们的 BFF 层。

BFF

我们需要定义defineEventHandler函数作为处理请求,并返回结果的函数。这里列出一个较为具体的代码。是不是非常简单?

// server/api/navs.js
import { useGraphql } from '~~/composables/graphql'
export default defineEventHandler(async () => {
  const reqQuery = `query{
      navs{
        data{
          id
          attributes{
            name
            url
            tag
          }
        }
      }
    }`
  return (await useGraphql(reqQuery)).navs.data
})

上面代码中的reqQuery就是graphql需要的请求参数,也就是GraphQL 语句。

吐槽:如今大部分Nuxt3教程对于Server/api如何写都没有较为全面的教程。笔者浏览了很多资料,只找到通过server/api请求固定数据的例子。

结语

本文通过较为清晰的思路讲解了如何在Nuxt3上构建BFF层获取Strapi的GraphQL数据。事实上,本文也是一篇抛砖引玉的文章,Strapi、Nuxt还有许多能力没有发掘,期待大家与我一起交流。

参考

  1. https://juejin.cn/post/7182019663004434488
  2. https://juejin.cn/book/7137945369635192836
Marlene

作者: Marlene

2024 © MarleneJ & 少轻狂