BFF 数据流转
通过访问 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;
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论