发光:将青少年网站带入现代的水疗中心

2020年2月21日 ·<!-- --> 13<!-- -->分钟阅读< / div >

通过鲁本小马丁内斯。

多年来,web社区一直在称赞单页应用程序(SPA)体系结构的好处。几乎去任何一个网络会议或流行的技术博客,你一定会遇到大量关于它们的好处的讨论。这也解释了为什么我们对它们如此着迷:每次浏览一个网站的不同页面时,等待加载和初始化整个HTML、JavaScript和样式表页面的速度很慢;按需加载少量JavaScript非常快。spa还允许我们避免页面加载之间出现可怕的空白屏幕,而是让我们提供更友好的加载屏幕。这使得spa比传统架构更适合连接稀缺的环境,如移动网络,甚至某些国家的桌面网络。如果进行了正确的优化,即使是第一个页面的加载也可以相当精简;我甚至都不会说提供快速浏览体验的众所周知的财务激励.简短的版本是:要走了快

然而,许多SPA讨论没有考虑到迁移到单一页面体系结构的过程是多么艰巨。

在Ok188bet金宝搏官网cupid,我们自2004年以来一直存在,具有相当小的(〜30人)工程团队在整个方面发电。我们的Web CodeBase将Ruby上的Ruby预测,您可能会感到惊讶(或适当地恐怖),以了解我们的技术堆栈依赖于我们呼叫酒吧的服务器端渲染的家庭成长的PHP样框架。我们的产品是最近,Pub,Vanilla JS,JQuery,CoffeScript,React,Reftux,Redux等,大部分地依赖于服务器端水合状态,代替适当的API。此外,有一个无数的可能死的代码路径,有条件地注入了可疑用品的第三方脚本。所以,当我们谈论遗产代码和技术债务时,这就是我们带到桌子的东西。

正如你们很多人所知道的,为彻底重写一个产品做出商业上的理由通常是困难的。那么,如何将一个比一些JavaScript程序员更老的web应用程序带入现代呢?事实上,一段时间以来,我们已经朝着这个目标取得了重大进展。React可以很容易地渲染到现有的应用程序中,这对帮助我们在2016年中期开始采用它至关重要,谢天谢地,今天我们的产品几乎完全是React驱动的。我还很高兴地告诉大家,我们所有的产品页面都是由api驱动的(尽管我们还没有达到graphql驱动的理想境界——后面会详细介绍)。这两者都是我们多年来一直在努力的缓慢而渐进的转变,这使得我们最终转向SPA变得非常容易。

但是,尽管有了这些巨大的进步,直到最近,我们仍然依赖于Pub来实际生成每个请求的每个HTML页面。这阻碍了我们做一些很酷的事情,比如让我们的程序员不必学习一门编造的语言(这对招聘非常有用)。所以大约6个月前,我开始着手一个项目,打算把我们从酒吧搬出去。我们从移动网络产品开始转变,并在3个月后将这些经验应用到桌面网络。这两个平台的任务列表很广泛,但也相当简单:

  • 在编译时生成一个HTML文件来服务我们的JavaScript包。
  • 确保第一次刷得足够快。
  • 使用代码分割来避免最终得到一个臃肿的包。
  • 为样板逻辑(如第三方脚本)确定更好的抽象。
  • 更新传统的原始JavaScript工具,依赖于React的直接DOM操作。
  • 消除我在此过程中发现的任何未使用的代码。

在这个大规模迁移的过程中,我做了一些观察,并设计了一些抽象概念,希望您会发现它们对清理旧代码和编写新代码都有帮助。

追踪失效代码

所以让我们开始用最合理的方式来解决这些要点:按字母顺序。在迁移遗留基础架构时,消除未使用的代码是一个巨大的目标。部分原因是理想主义(删除代码!),部分原因是实用主义(需要翻译的代码更少!)但一个令人难以置信的恼人挑战是,区分哪些遗留代码对保持我们的网站工作至关重要,哪些代码可以安全地删除,因为自从OkCupid托管期刊和论坛的日子以来,这些代码就不再相关了(是的,我们曾经这样做;188bet金宝搏官网不,我不能告诉你为什么我们认为这是个好主意)。然而,在我们的工具带中有一个非常基本的工具,它被证明是非常宝贵的,能够帮助我们找出哪些代码正在运行,哪些代码没有运行:分析!

甜,甜的沉默。

我偶然发现了很多代码,我的工程师前辈们有远见地在其中添加了分析事件。当我发现这些代码时,我会检查它们是否在过去一年左右被解雇,数量是多少,并判断是否可以删除违规代码。老实说,这让我想要在我写过的每个React组件中添加一个分析事件。谁来劝我别这么做。

如果一切都像发射一个分析事件那样简单就好了!

优化第一次油漆

// index.js——应用程序的主入口点
从“React”中导入React;
从"react-dom"中导入ReactDOM;

从“。/ API”中导入API;
从“。/ Helpers”中导入helper;
import App from "./ App ";
从"./app_error"中导入AppError;

const root = document.getElementById(“root”);

//加载绝对必需品。
API.loadCriticalData ()
.then(()=> alpers.initializeglobals())
不要犹豫(()= > ReactDOM。呈现(< App / >,根))
.catch((错误)=> acctdom.render(,root));

它所做的就是,加载对我们的应用非常重要的数据。然后,通过初始化一些全局库,使应用程序的其余部分可以使用这些数据。最后,它将应用程序呈现给DOM。如果这个过程中有任何失败,我们将退回到错误状态。

然而,这种方法有一个重要的缺点:在我们等待数据加载时呈现的是什么?我们不想向用户展示一个空白屏幕,而这发生 - 即使它只会每次会话发生一次,也显示用户一个空白屏幕永远不会理想。要打击此功能,我们将使用App Shell填充我们的模板文件:

/ / index . html

< >头
<标题> Ok188bet金宝搏官网Cupid < /名称>
> < /头
身体< >
< div id = "根" >
< div class = " some-loading-state”>

< / div >

反应最终将转变为“根”div,把里面的一切都抹去;在此之前,我们可以渲染其中的任何内容div我们想要的,包括应用程序shell,以便在此期间显示用户或noscript使用UI显示用户而不使用JavaScript。这确保了JavaScript通过网络加载、解析、编译,然后加载所需的数据时,用户不会被迫坐在空白屏幕前。在此处了解有关JavaScript启动性能的更多信息。

代码分离

确保第一次快速绘制的另一个关键方面是,确保我们的JavaScript包不会随着我们的单页应用添加越来越多的路线而膨胀。幸运的是,这些天,React提供了一些很棒的开箱即用的工具来防止这种情况的发生。即:React.lazy反应。悬念

如果你不熟悉React.lazy,它是最近发布的一个很棒的补充react@16.6.0,它允许我们根据需要动态加载任何组件。你不再需要第三方库来基于当前路径进行代码分割;相反,你可以这样做:

/ / Routes.jsx
从“React”中导入React;
import {Switch, Route} from "react-router-dom";

import Loading from“。/Loading”;//一些加载状态UI。

const Home = React.lazy(() => import("pages/ Home "));
const Login = React.lazy(() => import("pages/ Login "));
const signup = ract.lazy(()=>导入(“页面/注册”));

const Routes = () => (
<反应。悬念后备={<加载/ >}>
<转>

<路由精确路径=“/ login”组件= {login} />

< /开关>
< /反应。悬念>
);

这里我使用的是React Router的转变路线组件来处理在给定路径上呈现组件。它完全不知道围绕它发生的惰性加载逻辑,您可以选择使用任何库或不使用库来代替它。

这里发生的是转变组件将决定哪个路线基于当前路径呈现。当它找到一个匹配的时候路线将将组件传递给它组件道具。路由器不知道的是,在这种情况下,每个组件都是动态加载的只渲染一次反应。悬念实际上会推迟渲染,直到组件加载。当它加载时,它将呈现你传递给它的任何内容回退与此同时,道具。这意味着向SPA添加额外的路由对主包来说几乎没有成本!

在未来,React可能会允许我们使用反应。悬念在获取数据时暂停呈现(继电器, React的GraphQL框架已经实现了这个),或者发生任何其他异步操作。但它已经足够稳定,可以像我们一样用于生产环境中的代码分割!在这里了解更多关于React代码分割的信息

使用useScript的第三方脚本

在SPA迁移过程中,我发现自己做的最常见的事情之一就是粘贴一些第三方脚本,几乎所有脚本看起来都是这样的:

<!——谷歌Analytics
<脚本>
(函数(我,年代,o, g, r, a, m){我[' GoogleAnalyticsObject '] = r;我[r] = [r] | |函数(){
(i [r] .q = i [r] .q || [])。推(参数)},i [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('发送','pageview');

<!—结束谷歌分析

这些代码通常会像上面那样预先简化,但每个代码的要点都是:向主体添加一个script标签,从CDN加载一些第三方JavaScript,并运行一些相关代码。

但是其中一些脚本我们只是想在某些情况下加载(例如,仅针对登录用户,仅针对有广告的用户,仅针对具有更高数据连通性的用户,等等),在静态HTML模板中没有太多空间容纳这种细微差别。幸运的是,React Hooks最终成为了一个处理这个问题的好工具!我们最终得到的结果是这样的:

/ / useScript.js
import {useRef, useEffect} from " react ";

/**
*动态加载JavaScript。
* @param {string} URL - 要加载脚本的URL。
* @param {object} [选项= {}] - 脚本元素的任何覆盖。
* /
function useScript(url, options = {}) {
//创建脚本元素,一次。
const scriptRef = useRef(document.createElement("script"));
useEffect (() = > {
/ /基本设置。
const script = scriptRef.current;
脚本。type = " text / javascript”;
脚本。src = url;

//高级修改,和/或订阅事件。
种(选项)
.foreach((key)=>脚本[key] = props [key]);

//如有必要地添加到正文。
如果(! document.body.contains(脚本)){
document.body.appendChild(脚本);

}, [url选项]);


导出默认使用符号;

因为这些脚本通常需要更多的设置,我通常的方法是这样包装它们:

// umergoogleanalytics.js.
import {useState, useEffect, useRef} from " react ";
从“./usescript”导入usersctift;

const GOOGLE_ANALYTICS_SDK_URL = " //www.bizviewz.com/analytics.js ";

/**
*加载并使用Google Analytics SDK。
* @param {object} location -当前用户的位置。
* /
function useGoogleAnalytics(location = window.location) {
//跟踪加载状态。
const [hasloaded,sethasloaded] = useState(false);

//加载SDK。
const sdkoptions = uereerf({onload:()=> sethasloaded(true)});
useScript (GOOGLE_ANALYTICS_SDK_URL sdkOptions.current);

//初始化Google Analytics。
useEffect (() = > {
//如果SDK还没有加载,则退出。
如果(! hasLoaded) {
返回;


窗口。ga(“创建”、“UA-XXXXX-Y ', '汽车');
}, [hasLoaded]);

//跟踪页面视图。
Const path = location.pathname;
useEffect (() = > {
//如果SDK还没有加载,则退出。
如果(! hasLoaded) {
返回;


window.ga.current(“发送”,“PageView”);
},[路径,哈拉]);


出口默认useGoogleAnalytics;

这样,当我们需要使用它们时,它就像这样简单:

/ / Page.jsx
从“React-Router-DOM”导入{USELOCATION};
import useGoogleAnalytics from " ./useGoogleAnalytics ";

函数BasicPage () {
const位置= USELOCATION();
useGoogleAnalytics(位置);

回报(

<标题/ >
<主要/ >
<页脚/>
< / div >
);

注意:我通过位置作为参数,而不是放置useLocation在钩子内调用。令人沮丧的是,我很早就发现React Router的钩子会抛出运行时错误,如果你试图在一个路由器上下文,这已经证明了与我们的非水疗中心页面互操作性的麻烦。通过传递位置作为参数而不是呼叫useLocation直接在钩子中,我们就能够更容易地跨SPA和非SPA代码重用它。

这种加载脚本的方法可以很好地扩展任何数量的helper脚本,同时对正在扫描代码的开发人员来说仍然很容易阅读!要了解更多React Hooks,请点击这里查看我之前关于这个主题的博文

迁移vanilla javascript工具

/ / legacy_popover.js
const legacpopover = {
初始化({主题}){
const dom = document.getElementById(" popover-dom ");

//一些有趣的dom操作。
这一点。applyTheme (dom、主题);
},

主题applyTheme (dom) {
/*这里的附加逻辑*/
},
};

在我们的Pub模板中,我们有如下代码:

/ / index . html
< div id = " popover-dom " > < / div >
<脚本>
LegacyPopover.init ({
//↓↓用户数据可用!耶
主题:“% {user.preferences.theme}”
});

虽然没有一个尺寸适合的所有解决方案来迁移这样的事情,但我确实发现了一种在95%的情况下工作的方法,在这里和那里有一些调整。同样,这里的目标通常是尽可能地修改原始代码,以减少潜在错误的表面积。

对于这样的东西,我可以采用两种方法:我可以选择通过编程方式创建遗留实用程序需要的任何DOMdocument.createElement;或者我可以通过从其他地方获得的DOM节点引用。我通常会根据需要创造所需的DOM的复杂程度决定使用哪种方法。在这种情况下,我将选择参考方法。结果看起来像这样:

/ / legacy_popover.js
const legacpopover = {
init({dom,theme}){
//一些有趣的dom操作。
这一点。applyTheme (dom、主题);
},

主题applyTheme (dom) {
/*这里的附加逻辑*/
},
};

出口默认LegacyPopover;

那么在哪里dom节点从何而来?反应,当然!

/ / Popover.jsx
从“React”中导入React;
从“lodash/get”中导入get;
从“GraphQL-Tag”导入GQL;
从“@ apollo / ract-hooks”导入{imemerquery};

从" ./legacy_popover "中导入LegacyPopover;

// apollo查询以获取必需的用户数据。
const theme_query = gql`
查询getUserTheme($userid: String!) {
用户(id: $ userid) {
ID
偏好{
主题



”;

const Popover = React。备注(({userid}) => {
//加载用户主题首选项。
const {data} = uderquery(theme_query,{userid});
Const theme = get(data, " user.preferences.theme ");

//保持对DOM节点的引用。
const domref = useref(null);
useEffect (() = > {
//如果我们没有准备好,提前退出。
如果(! domRef。当前|| !主题){
返回;


//初始化legacpopover。
const dom = domRef.current;
LegacyPopover.init({Dom,主题});
},主题,domRef);

回报(

);
});

出口默认弹出窗口;

这里有几件事需要注意。首先,遗留工具(导入为legacypopover)在加载所需的数据和呈现DOM节点之前都不会初始化。其次,初始化应该在每次装入组件时只发生一次(除非主题应该改变),由于方式useEffect的工作原理。最后,我记住了组件,以避免不必要的重新渲染,因为那样会干扰DOM。

在一些情况下,我迁移的遗留实用程序处理了一些导航逻辑。在SPA中,最好是使用React Router导航,而不是通过锚标记,因为这些没有给我们很好的过渡。为此,我将传递遗留实用程序agoToUrl函数作为参数,它要么通过React Router的导航history.push或通过手动设置window.location在non-SPA环境中。

接下来是什么?

对OkCupid的网络团队正在面临的挑战感兴趣吗?188bet金宝搏官网我们招聘!

最初发表在https://tech.188bet金宝搏官网okcupid.com.2020年2月21日。

188bet金宝搏官网OkCupid科技博客

阅读来自工程团队的故事,这些故事每天连接着数百万人

188bet金宝搏官网OkCupid科技博客

阅读来自工程团队的故事,这些故事每天连接着数百万人

188bet金宝搏官网OkCupid科技博客

阅读来自工程团队的故事,这些故事每天连接着数百万人

媒介<!-- -->是一个开放的平台,17亿读者来寻找洞察力和动态的思维。在这里,专家和未被发现的声音相似地潜入任何主题的核心,并将新的想法带到表面上。<!-- -->了解更多

遵循对您有关的作家,出版物和主题,您将在您的主页和收件箱中看到它们。<!-- -->探索

如果你有故事要讲,有知识要分享,有观点要分享,欢迎回家。在任何话题上发表你的想法都是简单而免费的。<!-- -->开始博客

获取Medium应用程序

一个“在App Store下载”的按钮,如果点击它,你就会进入iOS App Store
一个“打开,谷歌播放”的按钮,如果你点击它,就会进入谷歌播放商店