我如何将航班搜索引擎从PHP重写为NodeJS

你好 我叫安德烈(Andrey),我是莫斯科一所技术大学的兼职研究生 非常谦虚 新手企业家和开发人员。 在本文中,我决定分享我从PHP切换到NodeJS的经验(由于它的简单性,我曾经很喜欢它,但是最终我讨厌我-我解释了为什么要削减)。 在这里可以给出非常琐碎且看似基本的任务,尽管如此,我个人还是在结识NodeJS和JavaScript的服务器端开发功能时很好奇地解决了这些任务。 我将尽力解释和清楚地说明PHP终于进入了夕阳,并让位于NodeJS。 对于某人来说,学习在Node中呈现HTML页面的某些功能甚至可能会很有用,而这原本根本不是从单词中适应的。


引言


在编写引擎时,我使用了最简单的技术。 没有包管理器,没有路由。 只有名称与请求的路由匹配的硬核文件夹以及每个文件夹中的index.php由PHP-FPM配置为支持进程池。 后来有必要使用Composer和Laravel,这对我来说是最后一根稻草。 在继续讨论为什么我甚至决定重写PHP至NodeJS的所有内容之前,我将向您介绍一些背景知识。


包装经理


在2018年底,我碰巧处理了一个用Laravel编写的项目。 有必要修复一些错误,对现有功能进行更改,并在界面中添加一些新按钮。 该过程从安装软件包和依赖项管理器开始。 在PHP中,使用了Composer。 然后,客户为服务器提供了1个内核和512 MB RAM,这是我第一次使用Composer。 在具有512 MB内存的虚拟专用服务器上安装依赖项时,该过程由于内存不足而崩溃。


?


对我来说,作为一个熟悉Linux并具有与Debian和Ubuntu合作的经验的人,解决此问题的方法很明显-安装SWAP文件(交换文件-对于不熟悉Linux管理的人)。 例如,一个没有经验的新手开发人员将他的第一个Laravel发行版安装在Digital Ocean上,就去控制面板并提高价格,直到依赖项安装由于内存分段错误而停止。 那NodeJS呢?
NodeJS拥有自己的软件包管理器-npm。 它更易于使用,更紧凑,甚至可以在RAM量最少的环境中工作。 通常,没有什么可责怪Composer来应对NPM的,但是,如果在安装软件包时出现任何错误,Composer会像普通的PHP应用程序一样崩溃,并且您将永远不会知道软件包的哪一部分已安装以及是否最终安装了结束。 通常,对于Linux管理员,崩溃的安装= Rescue模式下的闪回和dpkg --configure -a 。 到这些“惊喜”取代我的时候,我已经不喜欢PHP了,但是这些是我曾经对PHP的热爱的最后棺材。


长期支持和版本控制问题


还记得开发人员首次提出PHP7时引起什么样的炒作和惊奇吗? 生产率提高了2倍以上,某些组件提高了5倍! 还记得PHP第七版何时诞生? 以及WordPress获得的速度有多快! 那是2015年12月。 您是否知道PHP 7.0现在被认为是PHP的过时版本,强烈建议对其进行更新……不,不是7.1版,而是7.2版。 根据开发人员的说法,版本7.1已被剥夺了积极的支持,并且仅收到安全更新。 而在8个月后,这将停止。 它将随着有效的支持和7.2版一起停止。 事实证明,到今年年底,PHP将只有一个当前版本7.3。


当前的PHP版本


实际上,这并不是挑剔,如果我使用PHP 7.0编写的项目,我也不会将其归因于我离开PHP的原因*在打开它时并没有引起过时警告。 让我们回到依赖项安装失败的项目。 这是一个在2015年用PHP 5.6在Laravel 4上编写的项目。 似乎只有4年过去了,但是没有-一堆过时警告,模块过时,由于大量根引擎更新而通常无法升级到Laravel 5。


这不仅适用于Laravel。 尝试获取在第一版PHP 7.0的有效支持期间编写的所有PHP应用程序,并准备整夜花费大量时间寻找解决方案,以解决过时的PHP模块中出现的问题。 最后,一个有趣的事实:对PHP 7.0的支持比对PHP 5.6的支持更早地终止。 一秒钟。


那NodeJS呢? 我不会说这里的一切都好得多,并且NodeJS的支持期与PHP根本不同。 不,这里大致相同-每个LTS版本都支持3年。 但是NodeJS具有更多这些最新版本。


当前版本的NodeJS


如果您需要部署2016年编写的应用程序,请确保与此完全没有问题。 顺便说一下,仅今年4月将不再支持版本6。 前面有8、10、11和即将到来的12。


切换到NodeJS时遇到的困难和惊喜


也许我将从最激动人心的问题开始,关于如何在NodeJS中呈现HTML页面。 但是,让我们首先记住如何在PHP中完成此操作:


  1. 将HTML直接嵌入PHP代码中。 所有尚未达到MVC的新手也是如此。 因此,这是在WordPress中完成的,这绝对是可怕的。
  2. 使用MVC可以简化开发人员的交互,并提供某种将项目分成几部分的方法,但实际上,这种方法有时只会使所有事情变得复杂。
  3. 使用模板引擎。 最方便的选择,但不是PHP。 只需看一下在Twig或Blade中建议的带有大括号和百分比的语法。

我是将几种技术结合或合并在一起的强烈反对者。 HTML必须分开存在,样式必须分开存在,JavaScript必须分开存在(在React中,这通常看起来很怪异-HTML和JavaScript混合在一起)。 因此,对于喜欢我的偏好的开发人员来说,理想的选择是模板引擎。 很长时间以来,我都不需要在NodeJS上搜索它,因此我选择了Jade(PugJS)。 只需欣赏其语法的简单性即可:


  div.row.links div.col-lg-3.col-md-3.col-sm-4 h4.footer-heading . div.copyright div.copy-text 2017 - #{current_year} . div.contact-link span : a(href='mailto:hello@flaut.ru') hello@flaut.ru 

这里的一切都非常简单:我编写了一个模板,将其下载到应用程序中,对其进行了一次编译,然后在任何方便的时间在任何方便的地方使用它。 我认为,通过将HTML嵌入PHP代码中,PugJS的性能大约比渲染的性能高2倍。 如果在PHP的较早版本中,服务器在大约200-250毫秒内生成了一个静态页面,那么现在是90-120毫秒(我们不是在谈论PugJS的呈现,而是从页面请求到服务器对具有就绪HTML的客户端响应所花费的时间) 这是在应用程序启动阶段加载和编译模板及其组件的样子:


 const pugs = {} fs.readdirSync(__dirname + '/templates/').forEach(file => { if(file.endsWith('.pug')) { try { var filepath = __dirname + '/templates/' + file pugs[file.split('.pug')[0]] = pug.compile(fs.readFileSync(filepath, 'utf-8'), { filename: filepath }) } catch(e) { console.error(e) } } }) //       return pugs.tickets({ ...config }) 

它看起来非常简单,但是在使用Jade时,使用已经编译的HTML会有点复杂。 事实是,为了在页面上实现脚本,使用了一个异步函数,该函数从目录中获取所有.js文件,并将最后一次更改的日期添加到每个文件中。 该函数具有以下形式:


 for(let i = 0; i < files.length; i++) { let period = files[i].lastIndexOf('.') // get last dot in filename let filename = files[i].substring(0, period) let extension = files[i].substring(period + 1) if(extension === 'js') { let fullFilename = filename + '.' + extension if(env === 'production') { scripts.push({ path: paths.production.web + fullFilename, mtime: await getMtime(paths.production.code + fullFilename)}) } else { if(files[i].startsWith('common') || files[i].startsWith('search')) { scripts.push({ path: paths.developer.scripts.web + fullFilename, mtime: await getMtime(paths.developer.scripts.code + fullFilename)}) } else { scripts.push({ path: paths.developer.vendor.web + fullFilename, mtime: await getMtime(paths.developer.vendor.code + fullFilename)}) } } } } 

在输出中,我们得到一个具有两个属性的对象数组-文件的路径以及它在时间戳中最后一次编辑的时间(用于更新客户端缓存)。 问题在于,即使在从目录收集脚本文件的阶段,它们也都严格按字母顺序加载到内存中(因为它们位于目录本身中,并且文件从上到下-从头到尾被收集在其中)。 这导致这样一个事实,即首先加载了app.js文件,然后加载了带有polyfills的core.min.js文件,最后加载了vendor.min.js 。 这个问题很简单地解决了-非常平凡的排序:


 scripts.sort((a, b) => { if(a.path.includes('core.min.js')) { return -1 } else if(a.path.includes('vendor.min.js')) { return 0 } return 1 }) 

在PHP中,它们以字符串形式预写的JS文件的路径形式都具有奇特的外观。 简单但不切实际。


NodeJS将其应用程序保留在RAM中


这是一个巨大的优势。 一切都为我安排了,因此在服务器上并行且彼此独立地有两个单独的站点-开发人员版本和生产版本。 想象一下,我对开发站点上的PHP文件进行了一些更改,并且需要将这些更改推广到生产环境中。 为此,您需要停止服务器或放置一个“抱歉,技术工作”存根,并且此时将文件分别从开发人员文件夹复制到生产文件夹。 这会导致某种形式的停机时间,并可能导致转化损失。 对我来说,NodeJS 内存中应用程序的优点是对引擎文件的所有更改仅在重新启动后才能进行。 这非常方便,因为您可以复制所有必要的文件并进行更改,然后重新启动服务器。 该过程不超过1-2秒,并且不会导致停机。
例如,在nginx中使用了相同的方法。 您首先编辑配置,使用nginx -t检查nginx -t然后再通过service nginx reload进行更改


集群化NodeJS应用程序


NodeJS有一个非常方便的工具-pm2进程管理器 。 我们通常如何在Node中运行应用程序? 我们进入控制台并编写node index.js 。 一旦我们关闭控制台,该应用程序就会关闭。 至少在具有Ubuntu的服务器上会发生这种情况。 为了避免这种情况并使应用程序始终运行,只需使用简单的pm2 start index.js --name production命令将其添加到pm2中即可 。 但这还不是全部。 该工具允许监视( pm2 monit )和应用程序集群。


让我们记住用PHP组织流程的方式。 假设我们有nginx服务http请求,并且需要将请求传递给PHP。 您可以直接执行此操作,然后在每次请求时都会产生一个新的PHP进程,并在完成后将其杀死。 或者,您可以使用fastcgi服务器。 我想每个人都知道它是什么,不需要赘述,以防万一,我想澄清一下,PHP-FPM最常用作fastcgi,其任务是产生许多准备随时接受和处理新请求的PHP进程。 这种方法的缺点是什么?


首先,您永远不知道应用程序将消耗多少内存。 其次,您将始终受到最大进程数的限制,因此,随着流量的急剧增加,您的PHP应用程序将使用所有可用的内存和崩溃,或者在进程的允许限制内休息并开始杀死旧的进程。 可以通过将我不记得的动态设置为PHP-FPM配置文件中的哪个参数来防止这种情况,然后此时将根据需要生成尽可能多的进程。 但是,再次,基本的DDoS攻击将吞噬所有RAM,并占用您的服务器。 或者,例如,一个错误脚本将占用所有RAM,并且服务器将冻结一段时间(开发过程中有先例)。


NodeJS的根本区别在于应用程序不能消耗超过1.5 GB的RAM。 没有进程限制;只有内存限制。 这鼓励您编写尽可能轻量的程序。 此外,根据可用的CPU资源,计算出我们可以负担的群集数量非常简单。 建议每个内核上最多可悬挂一个集群(就像在nginx中一样,每个CPU内核最多​​可悬挂一个工作线程)。


PM2中的聚类


此方法的优点是PM2依次重新加载所有群集。 返回上一段,它讨论了重新启动期间的1-2秒停机时间。 在群集模式下,当您重新启动服务器时,您的应用程序将不会经历毫秒级的停机时间。


NodeJS是一把好瑞士刀


现在有这样一种情况,即PHP充当编写站点的语言,而Python充当爬网这些站点的工具。 NodeJS是2合1的,一方面是叉子,另一方面是汤匙。 您可以在同一应用程序中的同一服务器上编写快速而强大的应用程序和Web搜寻器。 听起来很诱人。 但是,您问如何实现呢? Google本身推出了官方的Chromium API-Puppeteer。 您可以启动Headless Chrome(无用户界面的浏览器-“ headless” Chrome),并获得对浏览器API的最大访问以抓取页面。 与Puppeteer一起使用的最简单,最方便的方法


例如,在我们的VKontakte组中,定期发布从独联体城市到各个目的地的折扣和特别优惠。 我们以自动模式为帖子生成图像,为了使它们漂亮,我们需要漂亮的图片。 我不喜欢绑定各种API并在数十个网站上创建帐户,因此我编写了一个简单的应用程序,该应用程序使用Google Chrome浏览器来模仿普通用户,该浏览器会在网站上浏览带有库存图片的内容,并随机选择关键字找到的图片。 我曾经为此使用Python和BeautifulSoup,但现在不再需要。 而且,Puppeteer的主要功能和优点是您甚至可以轻松地欺骗SPA网站,因为您可以使用一个完整的浏览器来理解和执行网站上的JavaScript代码。 这很简单:


 const browser = await puppeteer.launch({headless: true, args:['--no-sandbox']}) const page = (await browser.pages())[0] await page.goto(`https://pixabay.com/photos/search/${imageKeyword}/?cat=buildings&orientation=horizontal`, { waitUntil: 'networkidle0' }) 

因此,我们用3行代码启动了浏览器,并打开了包含图片的网站页面。 现在,我们可以在页面上选择一个带有图像的随机块,并为其添加一个类,稍后,我们可以以相同的方式打开图像,直接与图像本身一起进入页面以进一步加载:


 var imagesLength = await page.evaluate(() => { var photos = document.querySelectorAll('.search_results > .item') if(photos.length > 0) { photos[Math.floor(Math.random() * photos.length)].className += ' --anomaly_selected' } return photos.length }) 

回想一下用PhantomJS编写代码需要花费多少代码(顺便说一句,它已关闭并与Puppeteer开发团队进行了密切合作)。 如此出色的工具能否阻止任何人切换到NodeJS?


NodeJS提供基本的异步


这可以被认为是NodeJS和JavaScript的巨大优势,尤其是随着ES2017中async / await的问世。 与PHP不同,PHP的任何调用都是同步进行的。 我将举一个简单的例子。 以前,在搜索引擎中,页面是在服务器上生成的,但是必须使用JavaScript在客户端已经存在的页面上显示某些内容,但那时Yandex尚不能在网站上使用JavaScript,必须专门为其实现快照机制(页面快照)。使用Prerender。 快照存储在我们的服务器上,并根据请求将其分发给机器人。 难题在于这些图像是在3-5秒内生成的,这是完全不可接受的,并且会影响网站在搜索结果中的排名。 为了解决这个问题,发明了一种简单的算法:当机器人请求某个页面(我们已经拥有一个快照)时,我们只给它现有的快照,然后执行操作以在后台创建一个新的快照并替换它已经可用。 如何在PHP中完成:


 exec('/usr/bin/php ' . __DIR__ . '/snapshot.php -a ' . $affiliation_type . ' -l ' . urlencode($full_uri) . ' > /dev/null 2>/dev/null &'); 

绝对不要那样做。
在NodeJS中,这可以通过调用异步函数来实现:


 async function saveSnapshot() { getSnapshot().then((res) => { db.saveSnapshot().then((status) => { if(status.err) console.error(err) }) }) } /** *     await * ..    resolve()   */ saveSnapshot() 

简而言之,您不是要绕过同步,而是要决定何时使用同步代码执行以及何时使用异步。 而且真的很方便。 特别是当您了解Promise.all()的可能性时


飞行搜索引擎引擎本身的设计方式是,它向第二个服务器发送请求,该服务器收集和汇总数据,然后转向该服务器以获取准备发布的数据。 方向页面用于吸引自然流量。


例如,对于查询“俄罗斯莫斯科航班”,将发布一个页面,其中包含地址/机票/莫斯科/ saint-petersburg / ,并且它需要数据:


  1. 本月该方向的航空公司价格
  2. 未来一年该方向的航空公司价格(未来12个月每月的平均价格)
  3. 安排此方向的航班
  4. 从派遣城市出发的热门目的地-从莫斯科(链接)
  5. 到达城市的热门目的地是圣彼得堡(链接)

在PHP中,所有这些请求都是同步执行的-一个接一个。 每个请求的平均API响应时间为150-200毫秒。 我们将200乘以5,平均得到的秒数仅满足数据对服务器的请求。 NodeJS有一个很棒的函数Promise.all ,它并行执行所有请求,但是将结果一一写入。 例如,以上所有五个请求的执行代码如下所示:


 var [montlyPrices, yearlyPrices, flightsSchedule, originPopulars, destPopulars] = await Promise.all([ getMontlyPrices(), getYearlyPrices(), getFlightSchedule(), getOriginPopulars(), getDestPopulars() ]) 

并且我们在200-300毫秒内获得了所有数据,从而将页面的数据生成时间从1-1.5秒减少到了500毫秒。


结论


从PHP切换到NodeJS,使我对异步JavaScript更加熟悉,学习了如何使用Promise和异步/等待。 重写引擎之后,页面加载速度得到了优化,并且与引擎在PHP中显示的结果大不相同。 在本文中,我们还可以讨论如何使用简单的模块与NodeJS中的缓存(Redis)和pg-promise(PostgreSQL)一起使用,以及将它们与Memcached和php-pgsql进行比较,但是本文篇幅相当庞大。 而且知道她的写作才能,她的结构也很差。 本文的目的是通过使用一个曾经用PHP编写的真实项目的示例来吸引那些仍在使用PHP的开发人员的注意,这些开发人员仍然不了解NodeJS的乐趣以及基于它的基于Web的应用程序的开发,但是由于偏好它的所有者转到另一个平台。


我希望我能够传达我的想法,并能或多或少地将其思想表达在本材料中。 至少我尝试过:)


写任何评论-友好或愤怒。 我会回答任何建设性的。

Source: https://habr.com/ru/post/zh-CN444400/


All Articles