返回介绍

BFF 数据流转

发布于 2024-09-11 01:01:38 字数 8876 浏览 0 评论 0 收藏 0

通过访问 http://localhost:1337/api/layouts?populate=deep 可以拿到需要的数据。

不过这样的数据是有一些乱的,有几个可以优化的点:

  • 请求参数 populate=deep 是每次请求都需要带上的,因为需要所有深度的数据。
  • 最终需要的是 data 中的数据,layout 只有一个,不需要分页相关的部分(meta)。
  • 针对每个结构体,Strapi 为它们套上了 attributes 和 id,这个是不利于调用的,因为没有覆盖对应 ts 类型,会增加很多不必要的调试成本。
  • 每个结构体都加上了 createdAt、 publishedAt、updatedAt 三个字段,实际上是不需要这些字段的,随着接口层级的增加,过多不被使用的字段会增加接口的复杂度和可维护性。

CMS 接口优化

自定义返回

在 src/api/* 的目录下,存放着结构体接口的定义,其中 controllers 存放着接口的控制器,每当客户端请求路由时,操作都会执行业务逻辑代码并发回响应,可以在其中重写 api 的相关方法(find、findOne、 update 等)。

以 layout 为例,首先为 layout 接口加上默认的 populate=deep 参数,这样每次请求的时候就不用再加了。

src/api/layout/controllers/layout.js

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::layout.layout", ({ strapi }) => ({
  async find(ctx) {
    ctx.query = {
      ...ctx.query,
      populate: "deep",
    };
    const { data } = await super.find(ctx);
    return data;
  },
}));

访问 http://localhost:1337/api/layouts,可以看到不需要加 populate 参数就可以拿到联表的数据了。

然后针对上面提到的 attributes、id 和时间相关的字段定义两个深度遍历的函数来对应去除。

新建 src/utils/index.js

/**
 * 移除对象中自动创建的时间字段
 * @param obj
 * @returns
 */
const removeTime = (obj) => {
  const { createdAt, publishedAt, updatedAt, ...params } = obj || {};
  Object.getOwnPropertyNames(params).forEach((item) => {
    if (typeof params[item] === "object") {
      if (Array.isArray(params[item])) {
        params[item] = params[item].map((item) => {
          return removeTime(item);
        });
      } else {
        params[item] = removeTime(params[item]);
      }
    }
  });
  return params;
};

/**
 * 移除属性和 id
 * @param {*} obj
 * @returns
 */
const removeAttrsAndId = (obj) => {
  const { attributes, id, ...params } = obj || {};
  const newObj = { ...attributes, ...params };
  Object.getOwnPropertyNames(newObj).forEach((item) => {
    if (typeof newObj[item] === "object") {
      if (Array.isArray(newObj[item])) {
        newObj[item] = newObj[item].map((item) => {
          return removeAttrsAndId(item);
        });
      } else {
        newObj[item] = removeAttrsAndId(newObj[item]);
      }
    }
  });
  return newObj;
};

module.exports = {
  removeTime,
  removeAttrsAndId,
};

然后对 layout 的 find 函数返回的数据调用进行处理。

'use strict';

/**
 * layout controller
 */
const { removeTime, removeAttrsAndId } = require('../../../utils/index');

const { createCoreController } = require('@strapi/strapi').factories;

module.exports = createCoreController('api::layout.layout', ({ strapi }) => ({
  async find(ctx) {
    ctx.query = {
      ...ctx.query,
      populate: 'deep',
    };
    const { data } = await super.find(ctx);
    return removeAttrsAndId(removeTime(data[0]));
  }
}));

再访问 http://localhost:1337/api/layouts,可以只包含了需要的数据。

增加跨域限制

Strapi 的接口默认不做跨域限制,这样所有的域名都可以调用,安全性是存在问题的。

在 config/middlewares.js 中加上跨域的限制。

module.exports = [
  'strapi::errors',
  'strapi::security',
  {
    name: 'strapi::cors',
    config: {
      enabled: true,
      headers: '*',
      origin: ['http://localhost:3000', 'http://localhost:1337'],
    },
  },
  'strapi::poweredBy',
  'strapi::logger',
  'strapi::query',
  'strapi::body',
  'strapi::session',
  'strapi::favicon',
  'strapi::public',
];

BFF 接口定义

接口配置好以后还不能直接在页面中调用,需要配置一层 BFF 层,即服务于前端的数据层。

因为通常配置的数据是站在结构体的角度的,并不一定可以由前端调用,往往还需要复杂的数据处理。

为了提高数据层的复用程度,增加 BFF 层,将接口包一层,进行相关处理后,前端页面只调用定义的 BFF 层接口,不直接与配置的接口产生交互。

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

与静态页面类似,Nextjs 接口也采用文件约定式路由的方式进行配置,可以分为预定义路由、动态路由和全捕获路由,如下面的例子:

// ./pages/api/home/test.js => api/home/test 预定义路由
// ./pages/api/home/[testId].js => api/home/test, api/home/1, api/home/23 动态路由
// ./pages/api/home/[...testId].js => api/home/test, api/home/test/12 全捕获路由

如果一个相同的路由,比如 api/home/test,按照优先级来匹配三者,会按照预定义路由 > 动态路由 > 全捕获路由的顺序来匹配。

预定义路由是精准匹配,后两者只是模糊匹配,虽然也满足匹配场景,但是只是作为兜底,优先会以预定义路由为准。

下面来开发 BFF 层,首先定义一个接口层 pages/api/layout.ts。

因为会经常用到本地域名 和 CMS 域名,所以拿一个变量来存储它们,后续根据环境区分也很方便。

utils/index.ts

export const LOCALDOMAIN = 'http://127.0.0.1:3000';
export const CMSDOMAIN = 'http://127.0.0.1:1337';

安装 axios 和 lodash。

npm i axios 
npm i lodash
npm i --save-dev @types/lodash

使用过 Express 的人应该知道中间件的概念,Express 是基于路由和中间件的框架,通过链式调用的方式来对接口进行一些统一的处理。

开源社区有开发提供了 next-connect 的依赖来补全这部分的能力,先来安装一下依赖。

npm install next-connect

pages/api/layout.ts

import axios from 'axios';
import nextConnect from 'next-connect';
import type { NextApiRequest, NextApiResponse } from 'next';
import { ILayoutProps } from '@/components/layout';
import { CMSDOMAIN } from '@/utils';
import { isEmpty } from 'lodash';

const getLayoutData = nextConnect()
  // .use(any middleware)
  .get((req: NextApiRequest, res: NextApiResponse<ILayoutProps>) => {
    axios.get(`${CMSDOMAIN}/api/layouts`).then(result => {
      const {
        copy_right,
        link_lists,
        public_number,
        qr_code,
        qr_code_image,
        site_number,
        title,
      } = result?.data || {};
      res?.status(200).json({
        navbarData: {},
        footerData: {
          title,
          linkList: link_lists?.data?.map((item: any) => {
            return {
              title: item.title,
              list: item?.links?.data?.map((_item: any) => {
                return {
                  label: _item.label,
                  link: isEmpty(_item.link) ? '' : _item.link,
                };
              }),
            };
          }),
          qrCode: {
            image: `${CMSDOMAIN}${qr_code_image.data.url}`,
            text: qr_code,
          },
          copyRight: copy_right,
          siteNumber: site_number,
          publicNumber: public_number,
        },
      })
    })
  })

export default getLayoutData;
  • NextApiResponse 类型是 Nextjs 提供的 response 类型,它提供了一个泛型,来作为整个接口和后续请求的返回,可以把需要的数据类型作为泛型传进去,保证整体代码有 ts 的 lint。
  • 返回数据用的是 json,针对数据的响应,Nextjs 提供下面的响应 Api,可以根据自己的需求选用不同的响应 Api。

res.status(code) - 设置状态码的功能。code 必须是有效的 HTTP 状态码。
res.json(body) - 发送 JSON 响应。body 必须是可序列化的对象。
res.send(body) - 发送 HTTP 响应。body 可以是 a string,an object 或 a Buffer。 res.redirect([status,] path) - 重定向到指定的路径或 URL。status 必须是有效的 HTTP 状态码。如果未指定,status 默认为 “307” “临时重定向”。
res.revalidate(urlPath) - 使用 . 按需重新验证页面 getStaticProps。urlPath 必须是一个 string。

改造 layout 部分的数据注入,换用接口数据。

import type { AppProps, AppContext } from 'next/app';
import App from 'next/app';
import Head from 'next/head';
import axios from 'axios';
import { LOCALDOMAIN } from '@/utils';
import type { ILayoutProps } from '@/components/layout';
import Layout from '@/components/layout';
import '@/styles/globals.css'

const MyApp = (data: AppProps & ILayoutProps) => {
  const {
    Component, pageProps, navbarData, footerData
  } = data;
  return (
    <div>
      <Head>
        <title>A Demo for 官网开发实战</title>
        <meta
          name="description"
          content="A Demo for 官网开发实战"
        />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Layout navbarData={navbarData} footerData={footerData}>
        <Component {...pageProps} />
      </Layout>
    </div>

  )
}

MyApp.getInitialProps = async (context: AppContext) => {
  const pageProps = await App.getInitialProps(context);
  const { data = {} } = await axios.get(`${LOCALDOMAIN}/api/layout`)
  return {
    ...pageProps,
    ...data,
  }
}
export default MyApp;

访问 http://localhost:3000。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文