译者:Ashley Watkins, Royi Hagigi
书名门牌号:
https://engineering.fb.com/web/facebook-redesign/当他们考虑如何构筑两个捷伊应用软体领域:两个为当代应用程序结构设计的、具备使用者对Facebook(他们未知的)所有期许的机能,他们原有的控制技术栈难以支持他们所须要的近似于图形界面应用领域的觉得和操控性。完全改写是非常少见的,但在这种情况下,由于过去二十年来Web控制技术发生了很多变化,他们知道这是他们实现操控性和今后可持续产业发展目标的惟一有效途径。
今天,他们就撷取一下他们在解构Facebook.com时的经验和教训,使用React(一种用于构筑使用者界面的新闻稿式JavaScript库)和Relay(React的GraphQL插件)来解构Facebook.com。
https://react.docschina.org/1、序言
他们希望Facebook.com能够快速开启,加速积极响应,并提供更多度交互的新体验。虽然服务器端驱动力(server-driven)的插件能提供更多加速开启天数,但他们不坚信它能像插件驱动力(client-driven)的插件那般具备交互性和畅快性。不过,他们坚信他们能构筑两个插件驱动力的插件,并能提供更多具备竞争优势的加速开启天数。
但是Cubzac做两个插件优先选择的APP,这带来了一连串捷伊难题。他们须要加速复建中文网站,同时化解速率和其他使用者新体验难题,所以在今后一两年假若可持续的产业发展。在整个过程中,他们紧紧围绕着两个控制技术标语积极开展工作:
更少,尽量早。只提供更多所须要的天然资源,所以能在须要的时候及时处理送抵。服务于使用者新体验的工程建设新体验。他们合作开发的终极目标是为了他们的使用者。当思索使用者新体验的考验时,他们须要鼓励技师预设做恰当的事情来网络连接新体验需求。 他们应用领域这些准则来改良中文网站的五个基本要素:CSS、JavaScript、数据和路由器。2、深思css,弹出新机能
首先,他们通过改变撰写和构筑样式的方式,将主页上的CSS减少了80%。在新中文网站上,他们写的CSS与在应用程序上看到的CSS不同。当他们将CSS-like的JavaScript和组件写在一起时,构筑工具会将这些样式分割成单独的优化包。因此,新中文网站的CSS数量减少了,支持暗模式和动态字体大小以实现可访问性,并改善了图片的渲染操控性,同时让技师们合作开发更容易。
原子化的CSS,减少主页80%的CSS在他们的旧中文网站上加载主页时,加载了超过400KB的压缩CSS(2MB未压缩),但实际上只有10%的CSS被用于初始渲染。他们一开始并没有使用那么多的CSS,只是随着天数的推移而增加,很少做删减。之所以会出现这种情况,部分原因是每两个新机能都意味要添加捷伊CSS。
他们通过在构筑时生成原子化CSS来化解这个难题。原子化CSS有两个对数增长曲线,因为它与惟一的样式新闻稿的数量成正比,而不是与他们撰写的样式和机能的数量成正比。这使得他们能将整个中文网站中生成的原子型CSS合并到两个单一的、小的、共享的样式中。结果是新主页CSS下载量不到老中文网站的20%。
协同定位样式(Colocating styles)减少未使用的CSS,使其更容易维护CSS随着天数的推移而增长的另两个原因是他们很难识别各种CSS规则是否还在使用。Atomic CSS有助于缓解这一点的操控性影响,但独特的样式仍然会增加不必要的字节,所以他们的源代码中未使用的CSS会增加工程建设开销。现在,他们将他们的样式与他们的组件写在一起,这样就能将它们串联起来删除,并且只在构筑时将它们分割成单独的包。
他们还化解了另两个难题,CSS的优先选择级取决于顺序,当使用自动打包时,这一点尤其难以管理,因为自动打包会随着天数的推移而改变。以前,两个文件中的变化可能会在译者没有意识到的情况下破坏另两个文件中的样式。相反,他们现在用一种熟悉的语法来撰写样式,它的灵感来自于React Native风格的API。他们保证样式以稳定的顺序应用领域,所以不支持CSS后裔选择器。
改变字体大小以提高无障碍性在今天的许多中文网站上,人们会通过使用应用程序的缩放机能放大文字。这可能会不小心触发平板电脑或移动端布局,或者改变不须要放大的东西,比如图片。
通过使用rems,他们能遵守使用者指定的预设值,并且能够提供更多对自定义字体大小的控制,而不须要修改CSS。不过,结构设计通常是使用CSS像素值创建的。手动转换为rems会增加工程建设开销和潜在的bug,所以他们的构筑工具自动完成这个转换。
构筑处理例子const styles = stylex.create({ emphasis: { fontWeight: bold, }, text: { fontSize: 16px, fontWeight: normal, }, }); function MyComponent(props) { return <span className={styles(text, props.isEmphasized && emphasis)} />; }.c0 { font-weight: bold; } .c1 { font-weight: normal; } .c2 { font-size: 0.9rem; }function MyComponent(props) { return<span className={(props.isEmphasized ?c0 : c1 ) + c2 } />; } 用于主题结构设计的CSS变量(暗夜模式)在旧中文网站上,他们曾经尝试通过在body元素中添加两个类名来应用领域主题,然后用这个类名来覆盖原有的样式,这些样式有更高的优先选择级。这种方法有难题,它不再适用于他们捷伊原子化的CSS-in-JavaScript方法,所以他们改用CSS变量来进行主题切换。
CSS变量被定义在两个类下,当这个类应用领域到DOM元素上时,它的值会被应用领域到它的DOM子树中的样式。这让他们能将主题组合成两个单一的样式表,这意味着切换不同的主题不须要重新加载页面,不同的页面能有不同的主题而不须要下载额外的CSS,不同的产品能在同两个页面上并排使用不同的主题。
.light-theme { --card-bg: #eee; } .dark-theme { --card-bg: #111; } .card { background-color: var(--card-bg); }在JavaScript中使用SVG,实现加速、单一渲染的操控性为了防止图标在其他内容之后出现闪烁,他们使用 React 将 SVG 内联到 HTML 中,而不是将 SVG 以img的方式显示。因为这些SVG现在是有效的JavaScript,所以它们能和周围的组件一起实现干净的单次渲染。他们发现,在加载JavaScript的同时加载这些SVG的好处大于SVG的绘制操控性。通过内联,不会出现图标闪烁。
function MyIcon(props) { return ( <svg {...props} className={styles({/*...*/})}> <path d="M17.5 ... 25.479Z" /> </svg> ); }3、JavaScript通过Code-splitting提高操控性
代码大小是两个基于JavaScript的单页面应用领域最大的担忧之一,因为它对页面加载操控性影响很大。他们知道,如果他们想让Facebook.com的插件React app有插件的效果,就须要化解这个难题。他们引入了几个捷伊API,这些API的工作原理与他们 "更少,尽量早"的标语一致。
递增的代码加载,在须要的时候提供更多须要的东西(what we need, when we need it)在等待页面加载的时候,他们的目标是通过渲染页面的UI "骨架 "来即时反馈页面会是什么样子。这个骨架须要最少的天然资源,但如果代码被打成两个包,他们就难以提前渲染,所以他们须要根据页面显示的顺序将代码拆分成包。不过,如果简单地这样干(即使用在渲染过程中获取的动态导入),他们可能会伤害到操控性,而不是有利于操控性。这就是他们对“JavaScript加载层”的代码拆分结构设计的基础。他们将初始加载所需的JavaScript分成三层,使用两个新闻稿式的、可静态分析的API。
第1层是显示上层内容的首刷所需的基本布局,包括初始加载状态的UI骨架。
第1层使用常规的导入方式
import ModuleA from ModuleA;第2层包括了所有须要的JavaScript,以完全呈现所有的折叠内容。第2层之后,屏幕上的任何内容都不应该因为代码加载而发生视觉上的变化。
importForDisplay ModuleBDeferred from ModuleB;一旦遇到两个importForDisplay,它和它的依赖关系就会被移到第2层。返回两个基于promise包装的模块,以便在模块加载后访问它
第3层包含显示后才须要的、不影响当前屏幕展示的所有东西,包括log代码和订阅实时更新数据的代码。
importForAfterDisplay ModuleCDeferred from ModuleC; // ... function onClick(e) { ModuleCDeferred.onReady(ModuleC => { ModuleC.log(Click happened! , e); }); }两个500KB的JavaScript页面,在第1层能变成50KB,第2层能变成150KB,第3层能变成300KB。以这种方式分割代码,使他们能够通过减少须要下载的代码量来达到每两个里程碑,从而提高了从第一次绘制到视觉完成的天数。因为第3层并不影响屏幕上的像素,所以它并不是真正的渲染,最终的刷图完成天数更早。最重要的是,加载屏幕能够更早地渲染。
只有在须要的时候才加载的试验驱动力(experiment-driven)的依赖项他们经常须要渲染两个相同的UI的变体,例如在A/B测试中经常须要渲染两个相同的UI。最简单的方法是下载两个版本,但这意味着下载的代码可能永远不会被执行。两个稍微好一点的方法是在渲染时动态导入,但这可能会很慢。
相反,为了保持他们的 "更少,尽量早 "的标语,他们构筑了两个新闻稿式的API,能提前提醒他们这些决定,并将其编码到他们的依赖图中。当页面正在加载时,服务器能够检查试验,并只向下发送所需版本的代码。
const Composer = importCond(NewComposerExperiment, { true: NewComposer, false: OldComposer, });仅在须要时才加载的数据驱动力(data-driven)的依赖项那么在整个页面加载过程中,不是静态的代码分支怎么办?例如,将所有不同类型和组合的组件代码全部加载会大大增加页面的JavaScript大小。
这些依赖关系是在运行时根据后端返回的数据类型来决定的。他们使用Relay的两个新机能,根据返回的数据类型来表达须要哪些渲染代码。如果帖子都有两个附件,比如说照片,他们能用下面新闻稿的方式来描述须要 PhotoComponent 组件渲染照片。
... on Post { ... on PhotoPost { @module(PhotoComponent.js) photo_data } ... on VideoPost { @module(VideoComponent.js) video_data } }更赞的是,PhotoComponent 本身就把它须要的照片附件类型的数据精确地描述为片段,这意味他们甚至能把查询逻辑拆分出来。
使用JavaScript预算来防止代码蠕变分层和条件依赖关系能帮助他们交付每个阶段所需的代码,但他们还须要确保每个层的规模随着天数的推移保持在可控范围内。为了管理这个难题,他们引入了每个产品的JavaScript预算。
他们根据操控性目标、控制技术约束、产品考虑制定预算。同时根据产品边界和团队边界分配页面级预算,并根据产品边界和团队边界进行细分。共享基础设施(Shared infra)被添加到两个精心筛选的列表中,并给出了自己的预算。共享基础设施会计入所有页面的预算,但其中的模块是免费提供更多给产品团队使用的。对于延迟加载、有条件加载或交互时加载的代码也有预算。
他们为过程的每一步创建了相关的工具:
依赖关系图工具让他们更容易理解字节来自哪里,并识别出减少代码大小的机会。合并请求上的大小监控会显示大小回归 / 改良,并触发可定制的警报。通过交互式图表显示历史大小以及修订的变化情况。通过Dashboard帮助他们了解当前的大小与预算的关系。4、尽早实现数据获取(data-fetching)的当代化
作为这次改写的一部分,他们对中文网站上的数据获取的基础设施进行了当代化改造。虽然旧中文网站的一些机能使用 Relay 和 GraphQL 进行数据采集,但大部分数据获取都是作为服务器端 PHP 渲染的一部分。在新中文网站上,他们能够与他们的移动应用领域标准化,并确保所有的数据获取都通过GraphQL进行。由于Relay和GraphQL已经为他们处理了 "更少的 "工作,他们只须要做一些改变,以支持尽早获得他们所须要的数据。
初始请求预加载数据,以提高开启效率许多Web插件须要等到所有的JavaScript被下载并执行后才从服务器上获取数据。有了Relay,他们能静态地知道页面须要什么数据。这意味着,一旦他们的服务器收到页面的请求,它就能立即开始准备必要的数据,并与所需的代码并行下载。当页面可用时,他们会将这些数据与页面一起流转,这样插件就能避免额外的往返次数,更快地呈现最终的页面内容。
为减少往返次数和提高交互性的流数据(注:流数据具备五个特点:数据实时到达;数据到达次序独立,不受应用领域系统所控制;数据规模宏大且不能预知其最大值;数据一经处理,除非特意保存,否则不能被再次取出处理,或者再次提取数据代价昂贵。(来自网上的解释))
在最初加载Facebook.com时,有些内容可能会被隐藏或呈现在视口之外。例如,大多数屏幕上能容纳一到两个News Feed帖子,但他们不知道事先会容纳多少个。此外,使用者很有可能会滚动,在连载往返的过程中,逐一抓取每个故事须要天数。另一方面,他们在一次查询中获取的故事越多,查询的速率就越慢,这就导致查询天数越长,即使是第两个故事,也须要更长的视觉完成(Visually Complete)天数。
(注:视觉完成天数是指网页可见区域内的所有元素都被100%加载。)
为了化解这个难题,他们使用了两个内部的GraphQL扩展—@stream,将Feed连接流向插件,用于初始加载和后续滚动时的分页。这使得他们能在每两个feed故事准备好后,只需进行一次查询操作,就能将每两个feed故事逐一发送。
fragment HomepageData on User { newsFeed(first: 10) { edges @stream } ...AdditionalData }推迟暂不须要的数据不同部分的查询天数是不同的,例如,在查看个人资料时,获取两个人的姓名资料和照片相对来说比较快,但获取他们的Timeline内容则须要较长的天数。
为了在一次查询中获取这两种类型的数据,他们使用@defer,当积极响应的不同部分准备好后就能将其变成流数据。这让他们能够尽快用初始数据渲染大部分的UI,并为其余部分渲染加载状态。有了React Suspense就更容易了,因为他们能显式地结构设计加载状态,以确保流畅的、自上而下的页面加载新体验。
fragment ProfileData on User { name profile_picture { ... } ...AdditionalData @defer }5、定义路由器图加快导航速率
加速导航是单页应用领域的两个重要机能。当导航到两个捷伊路径时,他们须要从服务器上获取各种代码和数据来渲染目的页面。为了减少加载新页面时须要的网络往返次数,插件须要提前知道每条路线须要哪些天然资源。他们将其称为路由器图,每个条目称为路由器定义。
尽早获得路由器定义对于Facebook来说,这个路由器图太大了,难以一次性发送全部的。相反,他们在会话期间,随着新链接的呈现,动态地将路由器定义添加到路由器图中。路由器图和路由器器存在应用领域的最顶端,允许结合当前应用领域和路由器器的状态来驱动力应用领域级的状态决策,例如基于当前路由器的顶部导航栏或聊天标签的行为。
尽早预获取天然资源插件插件通常要等到React渲染两个页面后才会下载该页面所需的代码和数据。通常情况下使用React.lazy或类似的东西实现。由于这可能会使页面导航速率变慢,所以他们反而会在链接被点击之前就开始请求一些必要的天然资源。
为了提供更多更流畅的新体验,他们使用React Suspense转场来继续渲染上两个路由器,直到下两个路由器完全渲染完毕或暂停到下两个页面的UI骨架的 “友好 “的加载状态。这样做会减少很多干扰,所以它模仿了标准的应用程序行为。
代码和数据并行下载在新中文网站上他们做了很多懒加载代码,但如果他们懒加载两个路由器的代码,而这个路由器的数据抓取代码就在这个路由器的代码里面,最后就会出现串行加载的情况。
"传统 "的React / Relay app,加上懒加载的路由器,结果会是两次往返
为了化解这个难题,他们想出了EntryPoints,它是包裹代码分割点并将输入转化为查询的文件。这些文件非常小,对于任何能到达的代码拆分点都会提前下载。
代码和数据是并行提取的,让他们能在一次网络请求往返中下载这些
GraphQL查询仍然与视图写在一起,但EntryPoint封装了何时须要该查询以及如何将输入转化为恰当的变量。插件使用这些 EntryPoints 来自动决定何时请求,确保预设情况下恰当的发生。这有两个额外的好处,那就是创建两个单一的JavaScript函数,它包含了App中任何给定点的所有数据获取需求,能用于前面讨论的服务器预加载。
他们在这里讨论的许多变化并不是Facebook特有的。这些概念和模式能应用领域到任何框架或库的插件插件中。通过标准化他们的控制技术栈,他们已经能够重新思索如何以一种执行力强、可持续的方式引入人们想要的机能--即使是在工程建设和产品规模的运营过程中也是如此。
工程建设新体验的改善和使用者新体验的改善必须齐头并进,不能把操控性和可访问性看作是对输出机能的额外负担。通过优秀的API、工具和自动化,他们能帮助技师们更快地推进工作,并同时发布更好的、更高操控性的代码。为提高捷伊Facebook.com的操控性所做的工作非常广泛,他们预计很快会撷取更多关于这项工作的信息。要查看重新结构设计的内容,请访问facebook.com。它正在逐步推出,很快就会对大家开放。
❤️ 最后
如果你觉得这篇文章对你有帮助,麻烦点个「关注/转发」,让更多的人也能看到你的撷取!