Glow-Up:让一个十几岁的网站到温泉的现代世界
通过鲁本小马丁内斯。

多年来,网络社区已经称赞单页应用程序带来的好处(SPA)的体系结构。去任何web会议或流行的科技博客,你一定会遇到大量的讨论他们的好处。就解释了为什么我们为他们:等待加载和初始化一个整页的HTML, JavaScript,和样式表每次我们导航到另一个页面的网站很慢;快速加载JavaScript的少量需求。温泉也让我们避免这种可怕的flash-of-blank-screen页面加载之间,而不是让我们提供更友好的加载屏幕。这使得温泉等connectivity-scarce环境比传统架构适合移动网络,甚至桌面web在某些国家。如果正确地优化,甚至第一个页面加载可以很瘦;我甚至不会进入著名的金融激励提供一个快速的浏览体验。简短的版本是:要走了快。

然而,许多温泉讨论不考虑是多么艰难的过程可以迁移到一个单独的页面架构。
尤其当你处理一个成熟的应用程序中,数以百万计的用户和成堆的了解甚少的代码。理解为什么这不是谈论太多多最直言不讳的科技公司往往分为两类:要么是一个相对较新的创业,科技债务比例小;或者他们更与几乎无限的资源建立FAANG-type应对技术债务。
OkC188bet金宝搏官网upid,自2004年以来,我们一直在与一个相当小的里面(~ 30人)工程团队推动的事情。我们的web代码库早于Ruby on Rails,你也许会感到惊讶(或适当惊恐)学习我们的技术堆栈本土php非常依赖于服务器端框架呈现,我们称之为酒吧。直到最近,我们的产品是一个酒吧、香草JS, jQuery, Coffescript,反应,回流,回来的,,很大程度上依赖于服务器端水合状态的适当的api。此外,还有无数的可能还要灭亡经费问题迭出的无用代码路径和有条件的第三方脚本注入。所以,当我们谈论遗留代码和技术债务,这就是我们把桌子上。
很多人都知道,通常很难做一个产品的商业理由重写。所以你怎么把一个web应用程序比一些JavaScript程序员和把它发展到现代?好了,我们已经取得重大进展这一目标一段时间。这一事实的反应可以很容易地渲染到现有应用程序是至关重要的帮助我们开始采用它,早在2016年代中期,今天我们的产品几乎全React-powered谢天谢地。我也高兴地报告,我们所有的产品页面都由api驱动(尽管我们不GraphQL-powered乌托邦的努力向更之后)。这两个是缓慢,逐渐过渡,我们多年来一直在工作,这使我们最终搬到一个水疗变得容易得多。

但尽管如此巨大的进步,我们,直到最近仍依靠酒吧实际动态生成每个HTML页面,每一个要求。这是阻止我们做很酷的东西,如没有我们的程序员必须学习一门语言(招聘非常有用)。大约6个月前,我开始一个项目,让我们的酒吧。我们开始与我们的移动互联网产品,这种转变,把这些经验用于桌面web 3个月后。两个平台的任务列表是广泛但相当简单:
- 生成一个HTML文件编译为JavaScript包。
- 确保快速(足够的)第一次油漆。
- 使用代码分开,以避免结束了一个臃肿的包。
- 确定样板更好的抽象逻辑像第三方脚本。
- 更新遗留香草JavaScript工具依赖于直接的DOM操作反应。
- 我找到消除任何未使用的代码。
这种大规模迁移的过程中,我做了一些观察和设计几个抽象,我希望你会发现有用的,无论是在清理旧代码和编写新的代码。
跟踪死代码

让我们开始解决这些要点在最符合逻辑的方法:按字母顺序。消除未使用的代码是一个巨大的目标当迁移遗留的基础设施。部分这是理想主义的(耶删除代码!),部分这是务实(耶更少的代码翻译!)。但非常恼人的挑战是辨别之间遗留代码是保持我们的网站的关键工作,和什么代码可以安全地删除,因为它没有相关的日子以来OkCupid主办的杂志和论坛(是的,我们用来做;188bet金宝搏官网不,我不能告诉你为什么我们认为这是一个好主意)。然而,有一个非常基本的工具在我们的工具,找出代码运行和被证明是非常重要的并不是什么:分析!

有大量的代码我偶然发现了我的工程的祖先很有远见,添加分析事件。当我发现这些,我会检查他们是否被解雇了在过去一年左右的时间里,和在什么体积,使判断调用它的代码是否可以切除。这真的让我想添加一个分析的事件我所写的每一个反应组件。有人说我。
如果一切都那么简单发射一个解析事件!
优化第一次油漆
迁移到一个单页应用的另一个困难的方面是我们的web应用程序依赖于某些数据被保证在任何时候都可用在我们应用反应。虽然这很容易确保与服务器端数据水合作用,这是更具挑战性,搬到一个静态的HTML模板。摆脱所有这些依赖关系会非常繁琐,难以QA,所以我们必须发展策略来解决这个问题。最后,我们解决了一些看起来像这样:
/ /索引。js -我们的主要应用程序的入口点
进口的反应从“反应”;
从“进口ReactDOM react-dom”;
从“进口API。/ API”;
从“进口帮手。/帮手”;
从“进口程序。/ App”;
从“。/进口AppError app_error”;
const根= . getelementbyid(“根”);
/ /加载绝对必需品。
API.loadCriticalData ()
不要犹豫(()= > Helpers.initializeGlobals ())
= > ReactDOM光板(()。呈现(< App / >,根))
= > ReactDOM .catch(错误)。呈现(< AppError错误={错误}/ >,根));
这是什么,它加载的数据绝对是我们的应用程序的关键。然后,它使数据提供给应用程序的其余部分通过初始化一些全球图书馆。最后,它使应用程序DOM。在这个过程中如果有任何失败,我们回退一个错误状态。
这种方法,有一个显著的缺点:呈现什么当我们等待数据加载?我们不想给用户一个空白的屏幕,而这发生如果每个会话才发生一次,显示用户一个空白屏幕不会理想。为了解决这一问题,我们填充模板文件与应用程序壳:
/ / index . html
< html >
<头>
<标题> Ok188bet金宝搏官网Cupid < /名称>
< / >头
<身体>
< div id = "根" >
< div class = " some-loading-state " >
< div class = " some-flashy-loader " > < / div >
< / div >
< noscript >
打开JavaScript,蠢猪!
< / noscript >
< / div >
< /身体>
< / html >
反应最终渲染成“根”div
内,消灭一切;在这之前,我们可以提供任何在这div
我们希望,包括应用程序shell显示用户同时或noscript
有界面显示用户没有JavaScript。这将确保在JavaScript加载线,解析、编译、加载必要的数据,用户不是被迫坐在一个空白屏幕。了解更多关于JavaScript的启动性能。
代码分离

确保快速第一漆的另一个重要方面是确保我们的JavaScript包没有气球的大小作为我们的单页应用添加越来越多的路线。幸运的是这些天,反应提供了一些很棒的开箱即用的工具来防止这种情况的发生。即:React.lazy
和React.Suspense
。
如果你不熟悉React.lazy
,这是一个伟大的除了最近在被释放react@16.6.0
,它允许我们动态地加载任何组件的需求。不再需要第三方库代码分割基于当前路线;相反,你可以这样做:
/ / Routes.jsx
进口的反应从“反应”;
进口}{开关,路线从“react-router-dom”;
从“进口加载。/加载”;/ /加载状态界面。
const回家= React.lazy(() = >进口(页/ Home));
const登录= React.lazy(() = >进口(“页面/登录"));
const注册= React.lazy(() = >进口(“页面/注册"));
const路线= ()= > (
<反应。悬念后备={<加载/ >}>
<转>
<路线具体路径= " / "组件={回家}/ >
<路线具体路径= " /登录”组件={登录}/ >
<路线具体路径= " /注册”组件={注册}/ >
< /开关>
< / React.Suspense >
);
在这里我使用路由器的反应开关
和路线
组件处理呈现一个组件在一个给定的路径。发生了延迟加载的逻辑是完全无关的,你可以选择使用任何或没有图书馆。
这里发生了什么,开关
组件将决定它路线
呈现基于当前路径。当它找到一个匹配,路线
将呈现组件传递给它的组件
道具。路由器不知道是什么,在这种情况下,这些组件被动态加载的只有一次他们会呈现。React.Suspense
会暂停直到组件加载渲染。加载时,它会使你通过它回退
同时支持。这意味着添加额外的路线我们水疗没有代价的主包!
在不久的将来,很可能将允许我们使用反应React.Suspense
暂停呈现在获取数据时(继电器
反应的GraphQL框架,已经实现了这个),或任何其他异步操作发生。但它是足够稳定使用代码分割生产今天,喜欢我们!了解更多关于代码分离与反应。
第三方与useScript脚本

最常见的一件事,我发现自己做水疗迁移期间是粘贴在第三方脚本中,几乎所有的看起来是这样的:
< !——谷歌分析>
<脚本>
(函数(我,年代,o, g, r, a, m){我[' GoogleAnalyticsObject '] = r;我[r] = [r] | |函数(){
(我[r]。q = [r]。q | | []) .push(参数)},我[r]。l = 1 *新的日期();a = s.createElement (o),
m = s.getElementsByTagName (o) [0]; a.async = 1; a.src = g; m.parentNode.insertBefore (a, m)
})(窗口、文档“脚本”,“https://www.bizviewz.com/analytics.js”、“遗传算法”);
ga(“创建”、“UA-XXXXX-Y ', '汽车');
ga(“发送”,“页面”);
> < /脚本
< !——谷歌分析结束- - >
这些通常是pre-minified如上所述,但几乎每一个要点是:脚本标记添加到身体,从一个CDN负载一些第三方JavaScript,并运行一些相关的代码。
但是这些脚本的一些我们真的只是想加载在某些情况下(例如只有登录用户,只有为用户与广告,仅为用户提供更高的数据连接,等等),并没有太多的空间这样的细微差别在一个静态的HTML模板。幸运的是,反应钩子是一个很棒的工具来处理这个问题!我们结束了这样的东西:
/ / useScript.js
进口{useRef useEffect},从“反应”;
/ * *
*动态加载JavaScript。
* @param{}字符串url加载脚本的url。
* @param{对象}={}[选项]——任何重写脚本元素。
* /
函数useScript (url选项= {}){
/ /创建脚本元素,一次。
const scriptRef = useRef (document.createElement("脚本"));
useEffect (() = > {
/ /基本设置。
const脚本= scriptRef.current;
脚本。type = " text / javascript”;
脚本。src = url;
/ /先进的修改和/或订阅事件。
种(选项)
.forEach((关键)= >脚本(例子)=道具(例子));
/ /添加到身体如果有必要。
如果(! document.body.contains(脚本)){
document.body.appendChild(脚本);
}
},[url选项]);
}
出口默认useScript;
因为这些脚本通常需要更多的设置,通常的方法是把他们像这样:
/ / useGoogleAnalytics.js
进口{useState、useEffect useRef}从“反应”;
从“。/进口useScript useScript”;
const GOOGLE_ANALYTICS_SDK_URL = " / /www.bizviewz.com/analytics.js ";
/ * *
*加载和使用Google Analytics SDK。
* @param{对象}位置——当前用户的位置。
* /
函数useGoogleAnalytics(位置= window.location) {
/ /跟踪负载状态。
const [hasLoaded setHasLoaded] = useState(假);
/ /加载SDK。
const sdkOptions = useRef ({onload () = > setHasLoaded (true)});
useScript (GOOGLE_ANALYTICS_SDK_URL sdkOptions.current);
/ /初始化谷歌分析。
useEffect (() = > {
/ /退出如果SDK尚未加载。
如果(! hasLoaded) {
返回;
}
窗口。ga(“创建”、“UA-XXXXX-Y ', '汽车');
},[hasLoaded]);
/ /跟踪页面浏览量。
const path = location.pathname;
useEffect (() = > {
/ /退出如果SDK尚未加载。
如果(! hasLoaded) {
返回;
}
window.ga。当前(“发送”、“浏览”);
},路径,hasLoaded);
}
出口默认useGoogleAnalytics;
这样,当我们需要使用它们,这是这么简单:
/ / Page.jsx
从“进口{useLocation} react-router-dom”;
从“。/进口useGoogleAnalytics useGoogleAnalytics”;
函数BasicPage () {
const位置= useLocation ();
useGoogleAnalytics(位置);
回报(
< div >
<标题/ >
<主要/ >
<页脚/ >
< / div >
);
}
注意:我通过位置
作为一个参数,而不是把useLocation
称钩内。令人沮丧的是,我发现在很早的时候,反应路由器的钩子抛出一个运行时错误如果你试图以外的使用它们路由器
上下文,它证明了麻烦的互操作性与我们non-SPA页面。通过位置
作为一个参数,而不是打电话useLocation
直接在钩,我们能够更容易地重用在温泉和non-SPA代码。
这种方法加载脚本尺度对任意数量的辅助脚本,同时保持为开发者扫描通过代码容易阅读!学习更多的关于钩子反应,看到在我以前的博客主题。
迁移香草JavaScript工具
我们遇到了另一个问题是如何迁移遗留UI工具,依靠我们的一些香草的结合JavaScript和DOM操作手册。通常我们会有一些工具,看起来是这样的:
/ / legacy_popover.js
const LegacyPopover = {
init({主题}){
const dom = . getelementbyid (“popover-dom”);
/ /一些有趣的dom操作。
这一点。applyTheme (dom、主题);
},
主题applyTheme (dom) {
* / / *额外的逻辑
},
};
在我们酒吧的模板,我们会有一些代码看起来像这样:
/ / index . html
< div id = " popover-dom " > < / div >
<脚本>
LegacyPopover.init ({
/ /↓↓↓神奇可用↓↓↓用户数据!耶
主题:“% {user.preferences.theme}”
});
> < /脚本
虽然不是一个放之四海而皆准的解决方案迁移这样的事情,我找到了一个方法,在95%的情况下,有一些调整。同样,这里的目标通常是修改尽可能少的原代码,以减少潜在的bug的表面积。
我有两种方法可以是这样的:我可以选择以编程方式创建任何遗留工具需要通过的DOMdocument.createElement
;或者我可以通过DOM节点的引用从其他地方。我通常决定使用哪种方法取决于复杂的需要创建DOM。在这种情况下,我会选择的参考方法。结果是这样的:
/ / legacy_popover.js
const LegacyPopover = {
init ({dom,主题}){
/ /一些有趣的dom操作。
这一点。applyTheme (dom、主题);
},
主题applyTheme (dom) {
* / / *额外的逻辑
},
};
出口默认LegacyPopover;
所以在哪里dom
节点从何而来?反应,当然!
/ / Popover.jsx
进口的反应从“反应”;
进口从“lodash /得到”;
从“进口gql graphql-tag”;
从“@apollo / react-hooks”进口{useQuery};
从“。/进口LegacyPopover legacy_popover”;
/ /阿波罗的查询来获得必要的用户数据。
const THEME_QUERY = gql '
查询getUserTheme (userid美元:字符串!){
用户(id: $ userid) {
id
偏好{
主题
}
}
}
”;
const窗=反应。备忘录(({userid}) = > {
/ /加载用户主题偏好。
const{数据}= useQuery (THEME_QUERY {userid});
const主题=(数据,“user.preferences.theme”);
/ /保持DOM节点的引用。
const domRef = useRef(空);
useEffect (() = > {
如果我们不准备好了/ /过早出局。
如果(! domRef。当前| | !主题){
返回;
}
/ /初始化LegacyPopover。
const dom = domRef.current;
LegacyPopover.init ({dom, theme });
},主题,domRef);
回报(
< div id = " popover-dom“ref = {domRef} / >
);
});
出口默认弹出窗口;
这里有几件事要注意。首先,遗留工具(进口LegacyPopover
)才初始化数据需要加载和DOM节点呈现。其次,初始化应该只发生一次/组件(除非山主题
应该改变),由于路吗useEffect
的工作原理。最后,我memoize的组件,以避免不必要的重新渲染惹DOM。
有一些场合遗留工具我是迁移一些导航逻辑处理。在水疗中心,这将是更可取的导航使用路由器的反应而不是通过锚标签,因为这些不给我们很好的过渡。为此,我将通过遗留工具goToUrl
作为参数的函数,它要么导航通过路由器的反应history.push
在水疗中心或通过手动设置window.location
在non-SPA环境中。
接下来是什么?
虽然大多数OkCupid网站正在通过188bet金宝搏官网我们的新单页应用架构,仍然有很多工作要做,很多包优化探索我们已经迁移。其中,我们想在我们的大多数使用GraphQL页面,这样我们可以共享一个规范化的实体商店在我们的产品(即跨页)共享缓存的数据。此外,我们已经提前思考进步等网络应用技术服务人员长期本地缓存的资源,并不断优化我们的产品更多connectivity-scarce环境。我们相信这个新架构将使我们能够更快和更定期向我们的用户提供新的特性。对于我个人而言,我期待回去和删除很多,许多遗留代码。

感兴趣的挑战的网络团队OkCupid工作吗?188bet金宝搏官网我们招聘!
最初发表在https://tech.188bet金宝搏官网okcupid.com2020年2月21日。
188bet金宝搏官网OkCupid的工程团队负责匹配每天数以百万计的人。OkCupid科技博客上读到他们的故事188bet金宝搏官网

188bet金宝搏官网OkCupid科技博客
188bet金宝搏官网OkCupid的工程团队负责匹配每天数以百万计的人。OkCupid科技博客上读到他们的故事188bet金宝搏官网