乍一看到某个问题,你会觉得很简单,其实你并没有理解其复杂性。当你把问题搞清楚之后,又会发现真的很复杂,于是你就拿出一套复杂的方案来。实际上,你的工作只做了一半,大多数人也都会到此为止……。但是,真正伟大的人还会继续向前,直至找到问题的关键和深层次原因,然后再拿出一个优雅的、堪称完美的有效方案。—— 乔布斯
笔者在青训营的大项目开发中遇到了一些需求和坑,有所沉淀,特成此文,以作交流。
前言
本篇文章主要带领大家在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?
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。它是相对于Strapi默认的RESTful API 设计而言的。
- 只获取你想要的数据
- 多种数据,一次完成
- 请求直观,方便开发
- 服务解耦,节省沟通
在Strapi中使用GraphQL的优势
上图是使用Strapi获取部分数据的截图。可以看出有许多可以优化的地方:
- 请求参数populate=deep是每次请求都需要带上的,这样我们才可以请求更深一层的数据。
- 我们在这里需要的是data数据,不需要一些meta数据
- 响应结果的结构体有很多冗余项,例如无意义的
id
、data
、attributes
字段,他们会增加很多TS类型的成本,不必要的调试成本 - 每个结构体都加上了 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
}
接口优化
可以看到,我们请求得到的数据结构体非常冗余。“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还有许多能力没有发掘,期待大家与我一起交流。