Habr前端开发者日志:重构与反思

Habr前端开发者日志:重构与反思

我一直对 Habr 的内部结构、工作流程的结构、通信的结构、使用的标准以及通常如何编写代码感兴趣。 幸运的是,我得到了这样的机会,因为我最近成为了habra团队的一员。 以移动版本的小重构为例,我将尝试回答这个问题:在前台工作感觉如何。 程序中:Node、Vue、Vuex 和 SSR 以及来自 Habr 个人经验笔记的酱料。

关于开发团队,你首先需要了解的是我们的人数很少。 还不够 - 这是三个前锋,两个后卫和所有哈布尔 - 巴克斯利的技术领先。 当然,还有一名测试员、一名设计师、三名 Vadim、一把奇迹扫帚、一名营销专家和其他 Bumburum。 但哈布尔的消息来源只有六名直接贡献者。 这是相当罕见的——一个拥有数百万美元受众的项目,从外面看起来像一个巨大的企业,实际上看起来更像是一个舒适的初创公司,拥有尽可能扁平的组织结构。

与许多其他 IT 公司一样,Habr 宣扬敏捷理念、CI 实践,仅此而已。 但根据我的感觉,Habr 作为一个产品,更多的是波浪式的发展,而不是持续性的发展。 因此,在连续的几个冲刺中,我们勤奋地编码一些东西,设计和重新设计,破坏一些东西并修复它,解决问题并创建新的问题,踩耙子搬起石头砸自己的脚,以便最终将功能发布到生产。 然后会有一段平静期,一段重建时期,是时候做“重要但不紧急”象限中的事情了。

下面要讨论的正是这个“淡季”冲刺。 这次它包括对 Habr 移动版本的重构。 总的来说,该公司对它寄予厚望,未来它应该取代 Habr 化身的整个动物园,成为通用的跨平台解决方案。 有一天将会出现自适应布局、PWA、离线模式、用户定制以及许多其他有趣的东西。

我们来设定任务

有一次,在一次普通的站会上,一位前台谈到了移动版评论组件的架构问题。 考虑到这一点,我们以团体心理治疗的形式组织了一次微型会议。 大家轮流说哪里痛,他们把一切都记录在纸上,他们同情,他们理解,只是没有人鼓掌。 结果是列出了 20 个问题,这清楚地表明移动 Habr 的成功之路仍然漫长而荆棘。

我主要关心的是资源使用的效率以及所谓的流畅的界面。 每天,在回家-工作-回家的路上,我都会看到我的旧手机拼命地试图在提要中显示 20 个头条新闻。 它看起来像这样:

Habr前端开发者日志:重构与反思重构前的移动Habr界面

这里发生了什么? 简而言之,服务器以相同的方式向每个人提供 HTML 页面,无论用户是否登录。 然后客户端 JS 被加载并再次请求必要的数据,但针对授权进行了调整。 也就是说,我们实际上做了同样的工作两次。 界面闪烁,用户额外下载了一百个千字节。 细节上,一切看起来更加令人毛骨悚然。

Habr前端开发者日志:重构与反思旧的 SSR-CSR 方案。 授权只能在 C3 和 C4 阶段进行,此时 Node JS 不忙于生成 HTML 并且可以代理对 API 的请求。

一位 Habr 用户非常准确地描述了我们当时的架构:

手机版太垃圾了我就是这么说的。 SSR 和 CSR 的糟糕结合。

我们不得不承认这一点,无论多么悲伤。

我评估了这些选项,在 Jira 中创建了一张票,并在“现在很糟糕,做正确的事情”级别上进行了描述,并大致分解了任务:

  • 重用数据,
  • 最小化重绘次数,
  • 消除重复请求,
  • 让加载过程更加明显。

让我们重用数据

理论上,服务器端渲染旨在解决两个问题:不受搜索引擎方面的限制 SPA 索引 并改进指标 FMP (不可避免地恶化 TTI)。 最后在一个经典场景中 2013 年 Airbnb 制定 今年(仍然在 Backbone.js 上),SSR 是在 Node 环境中运行的同构 JS 应用程序。 服务器只是发送生成的布局作为对请求的响应。 然后再水合发生在客户端,然后一切都可以正常运行,无需重新加载页面。 对于 Habr 来说,与许多其他包含文本内容的资源一样,服务器渲染是与搜索引擎建立友好关系的关键要素。

尽管该技术问世已经过去六年多了,在这段时间里,前端世界确实已经过了不少水,但对于许多开发者来说,这个想法仍然笼罩在秘密之中。 我们没有袖手旁观,将支持 SSR 的 Vue 应用程序推出到生产环境,但遗漏了一个小细节:我们没有将初始状态发送给客户端。

为什么? 这个问题没有确切的答案。 要么他们不想增加服务器响应的大小,要么因为一堆其他架构问题,或者它根本没有成功。 无论如何,丢弃状态并重用服务器所做的一切似乎非常合适和有用。 这个任务其实很简单—— 状态只是简单地注入 到执行上下文中,Vue 会自动将其作为全局变量添加到生成的布局中: window.__INITIAL_STATE__.

出现的问题之一是无法将循环结构转换为 JSON (循环参考); 通过简单地将此类结构替换为平坦的对应结构即可解决。

此外,在处理 UGC 内容时,您应该记住,应将数据转换为 HTML 实体,以免破坏 HTML。 为了这些目的,我们使用 he.

最小化重绘

从上图中可以看出,在我们的示例中,一个 Node JS 实例执行两项功能:SSR 和 API 中的“代理”,其中发生用户授权。 这种情况导致JS代码在服务器上运行时无法进行授权,因为节点是单线程的,而SSR功能是同步的。 也就是说,当调用堆栈正忙于某些事情时,服务器根本无法向自身发送请求。 事实证明,我们更新了状态,但界面并没有停止抽搐,因为必须考虑到用户会话来更新客户端上的数据。 我们需要教会我们的应用程序将正确的数据置于初始状态,同时考虑到用户的登录。

问题的解决方案只有两种:

  • 将授权数据附加到跨服务器请求;
  • 将 Node JS 层拆分为两个单独的实例。

第一个解决方案需要在服务器上使用全局变量,第二个解决方案将完成任务的期限延长了至少一个月。

如何做出选择? 哈布尔经常沿着阻力最小的路径前进。 非正式地,人们普遍希望将从想法到原型的周期缩短到最短。 对产品的态度模式有点让人想起 booking.com 的假设,唯一的区别是 Habr 更加认真地对待用户反馈,并相信你作为开发人员可以做出这样的决定。

遵循这个逻辑和我自己快速解决问题的愿望,我选择了全局变量。 而且,正如经常发生的那样,你迟早必须为此付出代价。 我们几乎立即付款:我们在周末工作,清理了后果,写道 抛尸 并开始将服务器分为两部分。 这个错误非常愚蠢,并且涉及它的错误不容易重现。 是的,这确实是一种耻辱,但不管怎样,我的带有全局变量的 PoC 仍然在跌跌撞撞和呻吟中投入生产,并且在等待迁移到新的“双节点”架构时运行得相当成功。 这是重要的一步,因为正式目标已经实现——SSR 学会了提供一个完全可以使用的页面,并且 UI 变得更加平静。

Habr前端开发者日志:重构与反思第一阶段重构后的移动 Habr 界面

最终,移动版的SSR-CSR架构导致了这样的画面:

Habr前端开发者日志:重构与反思“双节点”SSR-CSR 电路。 Node JS API 始终为异步 I/O 做好准备,并且不会被 SSR 函数阻塞,因为后者位于单独的实例中。 不需要查询链#3。

消除重复请求

执行操作后,页面的初始渲染不再引发癫痫。 但在 SPA 模式下进一步使用 Habr 仍然引起了混乱。

由于用户流的基础是表单的转换 文章列表 → 文章 → 评论 反之亦然,首先优化该链的资源消耗非常重要。

Habr前端开发者日志:重构与反思返回帖子提要会引发新的数据请求

没有必要深挖。 在上面的截屏视频中,您可以看到应用程序在向后滑动时重新请求文章列表,并且在请求期间我们看不到文章,这意味着之前的数据在某处消失了。 看起来文章列表组件使用本地状态并在销毁时丢失它。 事实上,应用程序使用了全局状态,但 Vuex 架构是直接构建的:模块与页面绑定,而页面又与路由绑定。 此外,所有模块都是“一次性的”——每次后续访问该页面都会重写整个模块:

ArticlesList: [
  { Article1 },
  ...
],
PageArticle: { ArticleFull1 },

总的来说,我们有一个模块 文章列表,其中包含类型的对象 文章 和模块 页面文章,这是该对象的扩展版本 文章, 有点儿 文章全文。 总的来说,这个实现本身并没有什么可怕的——它非常简单,甚至有人可能会说很天真,但非常容易理解。 如果每次更改路线时都重置模块,那么您甚至可以忍受它。 但是,例如,在文章提要之间移动 /饲料 → /全部,保证丢弃与个人提要相关的所有内容,因为我们只有一个 文章列表,您需要将新数据放入其中。 这再次导致我们重复请求。

在收集了我能够挖掘到的有关该主题的所有内容后,我制定了一个新的状态结构并将其呈现给我的同事。 讨论很长时间,但最终赞成的论点超过了质疑,我开始实施。

解决方案的逻辑最好通过两个步骤来揭示。 首先我们尝试将 Vuex 模块与页面解耦并直接绑定到路由。 是的,商店中会有更多的数据,getter 会变得更复杂一些,但我们不会加载文章两次。 对于移动版本来说,这也许是最有力的论据。 它看起来像这样:

ArticlesList: {
  ROUTE_FEED: [ 
    { Article1 },
    ...
  ],
  ROUTE_ALL: [ 
    { Article2 },
    ...
  ],
}

但是,如果文章列表可以在多个路线之间重叠怎么办?如果我们想重用对象数据怎么办? 文章 渲染帖子页面,将其变成 文章全文? 在这种情况下,使用这样的结构会更符合逻辑:

ArticlesIds: {
  ROUTE_FEED: [ '1', ... ],
  ROUTE_ALL: [ '1', '2', ... ],
},
ArticlesList: {
  '1': { Article1 }, 
  '2': { Article2 },
  ...
}

文章列表 这里它只是一种文章存储库。 在用户会话期间下载的所有文章。 我们非常小心地对待它们,因为这些流量可能是在地铁站之间的某个地方通过痛苦下载的,我们绝对不想通过强迫用户加载他已经加载的数据来再次给用户带来这种痛苦下载了。 一个东西 文章ID 只是一个指向对象的 ID 数组(就好像“链接”) 文章。 这种结构允许您避免重复路由共有的数据并重用对象 文章 通过将扩展数据合并到其中来呈现帖子页面时。

文章列表的输出也变得更加透明:迭代器组件迭代带有文章 ID 的数组,并绘制文章预告组件,将 Id 作为 prop 传递,而子组件反过来从 文章列表。 当您转到发布页面时,我们会从以下位置获取已经存在的日期 文章列表,我们发出请求以获取丢失的数据并将其添加到现有对象中。

为什么这种方法更好? 正如我上面所写,这种方法对于下载的数据更加温和,并且允许您重用它。 但除此之外,它还为一些完全适合这种架构的新可能性开辟了道路。 例如,在文章出现时进行轮询并将其加载到提要中。 我们可以简单地将最新的帖子放在“存储”中 文章列表,将新 ID 的单独列表保存在 文章ID 并通知用户。 当我们单击“显示新出版物”按钮时,我们只需将新 ID 插入到当前文章列表数组的开头,一切都会神奇地进行。

让下载更愉快

重构蛋糕上的锦上添花是骨架的概念,它使得在缓慢的互联网上下载内容的过程不再那么令人厌恶。 没有对这个问题进行讨论;从想法到原型的过程实际上花了两个小时。 该设计实际上是自行绘制的,我们教我们的组件在等待数据时渲染简单、几乎不闪烁的 div 块。 主观上,这种加载方法实际上减少了用户体内压力荷尔蒙的数量。 骨架看起来像这样:

Habr前端开发者日志:重构与反思
哈布拉装载

反思

我在哈布雷工作了六个月,我的朋友们仍然会问:嗯,你喜欢那里吗? 好吧,舒服——是的。 但有一些东西使这项工作与其他工作不同。 我所在的团队对他们的产品完全漠不关心,不知道也不了解他们的用户是谁。 但这里一切都不同了。 在这里,你感到对自己所做的事情负责。 在开发功能的过程中,您部分成为该功能的所有者,参加与您的功能相关的所有产品会议,自己提出建议并做出决策。 制作一个你每天使用的产品非常酷,但是为那些可能比你更擅长的人编写代码只是一种令人难以置信的感觉(没有讽刺)。

所有这些更改发布后,我们收到了积极的反馈,这非常非常好。 这很鼓舞人心。 谢谢你! 多写点。

让我提醒您,在全局变量之后,我们决定更改架构并将代理层分配到单独的实例中。 “双节点”架构已经以公测的形式发布。 现在任何人都可以切换到它并帮助我们使移动 Habr 变得更好。 这就是今天的全部内容。 我很乐意回答您在评论中提出的所有问题。

来源: habr.com

添加评论