我们今天发布的翻译材料将集中于在从服务器接收的数据看起来不符合客户端需求的情况下该怎么做。 即,首先我们将考虑此类典型问题,然后将分析几种解决方法。

服务器API失败的问题
让我们考虑一个基于几个实际项目的条件示例。 假设我们正在为已经存在一段时间的组织开发一个新网站。 她已经有了REST端点,但是它们并不是针对我们要创建的对象而设计的。 在这里,我们仅需要访问服务器以验证用户身份,获取有关他的信息以及下载该用户未查看的通知列表。 因此,我们对服务器API的以下端点感兴趣:
/auth
:授权用户并返回访问令牌。/profile
:返回基本用户信息。/notifications
:允许您获取未读的用户通知。
想象一下,我们的应用程序始终需要以单个单位接收所有这些数据,也就是说,理想的情况是,如果我们只有三个端点而不是三个端点,那将是很好的选择。
但是,我们面临的问题比太多的端点要多得多。 特别是,我们谈论的是这样一个事实,即我们收到的数据看起来并不是最好的方式。
例如,端点
/profile
是在远古时代创建的,它不是用JavaScript编写的,因此,返回给它的数据中的属性名称对于JS应用程序来说看起来很不寻常:
{ "Profiles": [ { "id": 1234, "Christian_Name": "David", "Surname": "Gilbertson", "Photographs": [ { "Size": "Medium", "URLS": [ "/images/david.png" ] } ], "Last_Login": "2018-01-01" } ] }
总的来说-没有什么好。
的确,如果您查看端点
/notifications
产生的内容,那么
/profile
的上述数据将非常不错:
{ "data": { "msg-1234": { "timestamp": "1529739612", "user": { "Christian_Name": "Alice", "Surname": "Guthbertson", "Enhanced": "True", "Photographs": [ { "Size": "Medium", "URLS": [ "/images/alice.png" ] } ] }, "message_summary": "Hey I like your hair, it re", "message": "Hey I like your hair, it really goes nice with your eyes" }, "msg-5678": { "timestamp": "1529731234", "user": { "Christian_Name": "Bob", "Surname": "Smelthsen", "Photographs": [ { "Size": "Medium", "URLS": [ "/images/smelth.png" ] } ] }, "message_summary": "I'm launching my own cryptocu", "message": "I'm launching my own cryptocurrency soon and many thanks for you to look at and talk about" } } }
这里的消息列表是一个对象,而不是数组。 此外,此处存在用户数据,这些用户数据的配置与
/profile
端点的情况一样不舒服。 而且-令人惊讶的是
timestamp
属性包含自1970年初以来的秒数。
如果我不得不绘制一个刚才讨论过的那种令人讨厌的不便系统的体系结构图,它看起来就像下图所示。 红色用于该电路的那些部分,与准备不足的数据相对应,无法进行进一步的工作。
系统图在这种情况下,我们可能不会努力修复此系统的体系结构。 您可以简单地从这三个API加载数据并在应用程序中使用此数据。 例如,如果您需要在页面上显示完整的用户名,则需要结合使用
Christian_Name
和
Surname
属性。
在此,我想谈一谈名称。 将一个人的全名分为个人名称和姓氏的想法是西方国家的特征。 如果您正在开发供国际使用的产品,请尝试将该人的全名视为一个不可分割的字符串,并且不要对如何将此字符串分解成较小的部分进行任何假设,以使用发生在需要简洁或希望以非正式的方式吸引用户。
回到我们不完善的数据结构。 在这里可以看到的第一个明显问题是需要在用户界面代码中组合不同的数据。 其原因在于我们可能需要在几个地方重复此操作。 如果您仅需要偶尔执行此操作,则问题并不那么严重,但是如果您经常需要执行此操作,则情况会更糟。 结果,由于从服务器接收的数据的排列方式以及在应用程序中如何使用它们的不匹配而导致出现不希望的现象。
第二个问题是用于形成用户界面的代码的复杂性。 我认为,这样的代码首先应该尽可能简单,其次应该尽可能清晰。 您必须在客户端上执行的内部数据转换越多,其复杂性就越高,并且通常隐藏错误的地方就是复杂的代码。
第三个问题涉及数据类型。 从上面的代码段中,您可以看到,例如,消息标识符是字符串,而用户标识符是数字。 从技术角度来看,一切都很好,但是这样会使程序员感到困惑。 另外,请看日期显示! 但是,与个人资料图片相关的部分数据混乱了吗? 毕竟,我们所需要的只是一个指向相应文件的URL,而不是我们必须自己通过嵌套数据结构丛林创建的URL。
如果我们处理这些数据,并将其传递给用户界面代码,然后分析这些模块,我们将无法立即准确地了解我们正在使用的模块。 在使用内部数据结构及其类型时转换内部数据结构给程序员带来了额外的负担。 但是没有所有这些困难,这是完全可能的。
实际上,作为一种选择,有可能实现一个静态类型系统来解决此问题,但是严格的类型输入仅凭其存在的事实就不能使不良代码变好。
现在您可以看到我们面临的问题的严重性,让我们来讨论解决问题的方法。
解决方案#1:更改服务器API
如果现有API的不便之处不是由某些重要原因决定的,则没有什么可以阻止您创建更适合项目需求的新版本并找到该新版本,例如
/v2
。 也许可以将这种方法称为上述问题的最成功解决方案。 下图显示了这种系统的方案,以绿色突出显示了完全符合客户需求的数据结构。
新的服务器API可以准确产生系统客户端的需求开始开发一个新项目,该项目的API仍有很多不足之处,我一直对实现上述方法的可能性始终感兴趣。 但是,有时API设备尽管很不方便,却具有一些重要的目标,或者根本不可能更改服务器API。 在这种情况下,我采用以下方法。
解决方案2:BFF模式
这是一个很好的旧BFF(
Backend-For-the-Frontend )模式。 使用此模式,您可以从复杂的通用REST端点中抽象出来,并为前端提供所需的确切信息。 这是这种解决方案的示意图。
应用BFF模式BFF层存在的意义是为了满足前端的需求。 也许他会使用其他REST终结点,GraphQL服务,Web套接字或其他任何方式。 它的主要目标是为了使应用程序的客户端方便而做所有可能的事情。
我最喜欢的体系结构是NodeJS BFF,前端开发人员可以使用该体系结构完成他们需要的工作,并为他们开发的客户端应用程序创建出色的API。 理想情况下,相应的代码与前端本身的代码位于同一存储库中,从而简化了代码共享,例如,用于在客户端和服务器上检查发送的数据。
另外,这意味着需要更改应用程序的客户端部分及其服务器API的任务在一个存储库中执行。 就像他们说的那样琐碎,但是很好。
但是,不一定总是使用BFF。 这个事实使我们找到了方便使用不良服务器API的另一种解决方案。
解决方案#3:BIF模式
BIF(前端后端)模式使用的逻辑与使用BFF(组合多个API和数据清除)时可以应用的逻辑相同,但是此逻辑移至客户端。 实际上,这个想法并不新奇,它可能在20年前就已经出现过,但是这种方法可以帮助使用组织不良的服务器API,这就是我们在谈论它的原因。 这是它的外观。
应用BIF模式▍什么是BIF?
从上一节可以看出,BIF是一种模式,即一种理解代码及其组织的方法。 它的使用不会导致需要从项目中删除任何逻辑。 它只是将一种类型的逻辑(数据结构的修改)与另一种类型的逻辑(用户界面的形成)分开。 这类似于每个人都听到的“责任分工”的想法。
在这里,我想指出,尽管这不能算是一场灾难,但我经常不得不看到文盲的BIF实现。 因此,在我看来,很多人都会有兴趣听听有关如何正确实施此模式的故事。
BIF代码应被视为可以一次获取并传输到Node.js服务器的代码,此后一切将以与以前相同的方式工作。 甚至将其转移到私有NPM软件包中,将在一个公司框架内的多个前端项目中使用,这简直令人惊讶。
回想一下,我们上面讨论了使用失败的服务器API时出现的主要问题。 其中一个是对API的调用过于频繁,并且它们返回的数据不能满足前端的需求。
我们将针对每个问题的解决方案分成单独的代码块,每个代码块将放置在其自己的文件中。 结果,应用程序客户端部分的BIF层将由两个文件组成。 此外,还将向他们附加一个测试文件。
▍结合API调用
在我们的客户端代码中对服务器API进行大量调用并不是一个严重的问题。 但是,我想对其进行抽象,以使其能够完成一个“请求”(从应用程序代码到BIF层),并确切地获得响应所需的内容。
当然,在我们的情况下,向服务器发出三个HTTP请求是不可避免的,但是应用程序不需要知道它。
我的BIF层的API表示为函数。 因此,当应用程序需要有关用户的一些数据时,它将调用
getUser()
函数,该函数将向其返回数据。 该函数的外观如下:
import parseUserData from './parseUserData'; import fetchJson from './fetchJson'; export const getUser = async () => { const auth = await fetchJson('/auth'); const [ profile, notifications ] = await Promise.all([ fetchJson(`/profile/${auth.userId}`, auth.jwt), fetchJson(`/notifications/${auth.userId}`, auth.jwt), ]); return parseUserData(auth, profile, notifications); };
在这里,首先,向身份验证服务发出请求以获取令牌,该令牌可用于授权用户(在此我们将不讨论身份验证机制,但我们的主要目标是BIF)。
收到令牌后,您可以同时执行两个请求,以接收用户个人资料数据和有关未读通知的信息。
顺便说一下,看看使用
Promise.all
并使用破坏性赋值处理
async/await
结构时,其外观如何美丽。
因此,这是第一步,在这里我们从对服务器的访问包括三个请求这一事实中抽象出来。 但是,此案尚未完成。 即,请注意对
parseUserData()
函数的调用,正如您从其名称可以判断的那样,该函数会
parseUserData()
从服务器接收的数据。 让我们谈谈她。
▍数据清理
我想立即提出一个建议,我认为这会严重影响以前没有BIF层的项目,特别是新项目。 暂时不要考虑从服务器中获得什么。 相反,请专注于应用程序需要哪些数据。
此外,在设计应用程序时,最好不要尝试考虑它可能与2021年有关的未来需求。 只需尝试使应用程序完全按照今天的方式工作即可。 事实是,过度的规划热情和试图预测未来是导致软件项目不合理复杂化的主要原因。
因此,回到我们的业务。 现在我们知道了从这三个服务器API接收到的数据是什么样的,并且我们知道它们在分析后应该变成什么。
似乎这是使用TDD确实有意义的罕见情况之一。 因此,我们将为
parseUserData()
函数编写一个大型的长测试:
import parseUserData from './parseUserData'; it('should parse the data', () => { const authApiData = { userId: 1234, jwt: 'the jwt', }; const profileApiData = { Profiles: [ { id: 1234, Christian_Name: 'David', Surname: 'Gilbertson', Photographs: [ { Size: 'Medium', URLS: [ '/images/david.png', ], }, ], Last_Login: '2018-01-01' }, ], }; const notificationsApiData = { data: { 'msg-1234': { timestamp: '1529739612', user: { Christian_Name: 'Alice', Surname: 'Guthbertson', Enhanced: 'True', Photographs: [ { Size: 'Medium', URLS: [ '/images/alice.png' ] } ] }, message_summary: 'Hey I like your hair, it re', message: 'Hey I like your hair, it really goes nice with your eyes' }, 'msg-5678': { timestamp: '1529731234', user: { Christian_Name: 'Bob', Surname: 'Smelthsen', }, message_summary: 'I\'m launching my own cryptocu', message: 'I\'m launching my own cryptocurrency soon and many thanks for you to look at and talk about' }, }, }; const parsedData = parseUserData(authApiData, profileApiData, notificationsApiData); expect(parsedData).toEqual({ jwt: 'the jwt', id: '1234', name: 'David Gilbertson', photoUrl: '/images/david.png', notifications: [ { id: 'msg-1234', dateTime: expect.any(Date), name: 'Alice Guthbertson', premiumMember: true, photoUrl: '/images/alice.png', message: 'Hey I like your hair, it really goes nice with your eyes' }, { id: 'msg-5678', dateTime: expect.any(Date), name: 'Bob Smelthsen', premiumMember: false, photoUrl: '/images/placeholder.jpg', message: 'I\'m launching my own cryptocurrency soon and many thanks for you to look at and talk about' }, ], }); });
这是函数本身的代码:
const getPhotoFromProfile = profile => { try { return profile.Photographs[0].URLS[0]; } catch (err) { return '/images/placeholder.jpg'; // } }; const getFullNameFromProfile = profile => `${profile.Christian_Name} ${profile.Surname}`; export default function parseUserData(authApiData, profileApiData, notificationsApiData) { const profile = profileApiData.Profiles[0]; const result = { jwt: authApiData.jwt, id: authApiData.userId.toString(), // ID name: getFullNameFromProfile(profile), photoUrl: getPhotoFromProfile(profile), notifications: [], // , }; Object.entries(notificationsApiData.data).forEach(([id, notification]) => { result.notifications.push({ id, dateTime: new Date(Number(notification.timestamp) * 1000), // , , , Unix, name: getFullNameFromProfile(notification.user), photoUrl: getPhotoFromProfile(notification.user), message: notification.message, premiumMember: notification.user.Enhanced === 'True', }) }); return result; }
我想指出的是,当有可能在一个地方收集两百行代码来修改整个应用程序中分散的数据时,它会带来一种美妙的感觉。 现在,所有这些都保存在一个文件中,为此代码编写了单元测试,并且为所有含糊的时刻提供了注释。
我之前说过,BFF是我最喜欢的合并和清除数据的方法,但是在某些方面BIF优于BFF。 即,从服务器接收的数据可能包括不支持JSON的JavaScript对象,例如
Date
或
Map
对象(也许这是使用最多的JavaScript功能之一)。 例如,在本例中,我们必须将来自服务器的日期(以秒表示,而不是毫秒表示)转换为
Date
类型的JS对象。
总结
如果您认为您的项目与我们检查不成功的API问题有一个共同点,请询问以下有关在客户端上使用服务器数据的问题,以分析其代码:
- 您是否必须合并从未单独使用的属性(例如,用户的名字和姓氏)?
- JS代码是否必须使用以JS不接受的方式形成的属性名称(类似于PascalCase)?
- 各种标识符的数据类型是什么? 也许有时是字符串,有时是数字?
- 日期如何在您的项目中显示? 也许有时候这些
Date
JS对象可以在界面中使用了,有时候是数字,甚至是字符串? - 在开始枚举该实体的元素以根据其形成用户界面的某些片段之前,是否经常需要检查属性是否存在,或者检查实体是否为数组? 难道这个实体即使是空的也不会是数组吗?
- 形成接口时,是否必须对数组进行排序或过滤,理想情况下,该接口应该已经正确排序和过滤了?
- 事实证明,在检查属性是否存在时,没有寻找任何属性,是否必须切换为使用某些默认值(例如,当从服务器接收的数据中没有用户照片时,使用标准图片)?
- 属性是否统一命名? 是否发生同一实体可以具有不同名称的情况,这可能是由相对而言“旧”和“新”服务器API的联合使用引起的?
- 您是否必须将有用的数据与有用数据一起传输到某处从未使用过的地方,仅因为它来自服务器API才这样做? 这些未使用的数据会干扰调试吗?
如果您可以肯定地回答此列表中的一个或两个问题,那么也许您不应该修复已经可以正常工作的东西。
但是,如果您阅读这些问题,然后在每个问题中找出您的项目问题,如果代码的设备由于所有这些而不必要地复杂,如果难以感知和测试,是否包含难以检测的错误,请看一下BIF模式。
最后,我想说的是,当将BIF层引入现有应用程序时,事情变得更加容易,因为这可以分阶段,一步一步地完成。 假设用于准备数据的函数的第一个版本称为
parseData()
,它可以简单地,无需更改地返回其输入内容。 然后,您可以将逻辑从负责创建用户界面的代码逐渐移到该功能。
亲爱的读者们! , BIF?
