Hola Mi nombre es Andrey, soy estudiante de posgrado en una de las universidades técnicas de Moscú y a tiempo parcial muy modesto Emprendedor y desarrollador novato. En este artículo, decidí compartir mi experiencia de cambiar de PHP (que una vez me gustó por su simplicidad, pero eventualmente me odiaron; explico por qué bajo el corte) a NodeJS. Aquí se pueden dar tareas muy triviales y aparentemente elementales, que, sin embargo, tenía curiosidad personal por resolver durante mi conocimiento de NodeJS y las características del desarrollo del lado del servidor en JavaScript. Trataré de explicar y demostrar claramente que PHP finalmente ha entrado en la puesta del sol y ha dado paso a NodeJS. Tal vez incluso sea útil para alguien aprender algunas características de la representación de páginas HTML en Node, que originalmente no está adaptado a esto de la palabra.
Introduccion
Mientras escribía el motor, utilicé las técnicas más simples. Sin gestores de paquetes, sin enrutamiento. Solo carpetas hardcore, cuyo nombre coincide con la ruta solicitada, e index.php en cada una de ellas, configuradas por PHP-FPM para admitir el grupo de procesos. Más tarde se hizo necesario usar Composer y Laravel, que fue el colmo para mí. Antes de pasar a la historia de por qué incluso decidí reescribir todo, desde PHP hasta NodeJS, te contaré un poco sobre los antecedentes.
Administrador de paquetes
A finales de 2018, pasé a trabajar con un proyecto escrito en Laravel. Fue necesario corregir varios errores, realizar cambios en la funcionalidad existente, agregar algunos botones nuevos en la interfaz. El proceso comenzó instalando el paquete y el administrador de dependencias. En PHP, Composer se usa para esto. Luego, el cliente proporcionó un servidor con 1 núcleo y 512 megabytes de RAM y esta fue mi primera experiencia con Composer. Al instalar dependencias en un servidor privado virtual con 512 megabytes de memoria, el proceso se bloqueó debido a la falta de memoria.

Para mí, como persona familiarizada con Linux y con experiencia trabajando con Debian y Ubuntu, la solución a este problema era obvia: instalar un archivo SWAP (archivo de intercambio) para aquellos que no están familiarizados con la administración de Linux). Un desarrollador inexperto novato que instaló su primera distribución de Laravel en Digital Ocean, por ejemplo, solo va al panel de control y aumenta la tarifa hasta que la instalación de dependencias se detiene con un error de segmentación de memoria. ¿Qué hay de NodeJS?
Y NodeJS tiene su propio administrador de paquetes: npm. Es mucho más fácil de usar, más compacto, puede funcionar incluso en un entorno con una cantidad mínima de RAM. En general, no hay nada que culpar a Composer contra NPM, sin embargo, en caso de errores al instalar paquetes, Composer se bloqueará como una aplicación PHP normal y nunca sabrá qué parte del paquete se instaló y si se instaló al final termina En general, para el administrador de Linux, la instalación dpkg --configure -a
= flashbacks en modo de rescate y dpkg --configure -a
. Para cuando me sorprendieron tales "sorpresas", no me gustaba PHP, pero estos fueron los últimos clavos en el ataúd de mi gran amor por PHP.
Soporte a largo plazo y problemas de versiones
¿Recuerdas qué tipo de exageración y asombro causó PHP7 cuando los desarrolladores lo presentaron por primera vez? ¡Aumente la productividad en más de 2 veces, y en algunos componentes hasta 5 veces! ¿Recuerdas cuando nació la séptima versión de PHP? ¡Y qué tan rápido ganó WordPress! Era diciembre de 2015. ¿Sabía que PHP 7.0 ahora se considera una versión obsoleta de PHP y se recomienda actualizarlo ... No, no a la versión 7.1, sino a la versión 7.2. Según los desarrolladores, la versión 7.1 ya está privada de soporte activo y solo recibe actualizaciones de seguridad. Y después de 8 meses, esto se detendrá. Cesará, junto con el soporte activo y la versión 7.2. Resulta que para fines de este año, PHP solo tendrá una versión actual: 7.3.

De hecho, esto no sería una trampa y no atribuiría esto a las razones de mi partida de PHP si los proyectos que escribí en PHP 7.0. * Ya no causó una advertencia de desaprobación cuando lo abrí. Volvamos al proyecto donde se bloqueó la instalación de dependencias. Este fue un proyecto escrito en 2015 en Laravel 4 con PHP 5.6. Parecía que solo habían pasado 4 años, pero no: un montón de advertencias de desaprobación, módulos obsoletos, la incapacidad de actualizar a Laravel 5 normalmente debido a un montón de actualizaciones del motor raíz.
Y esto se aplica no solo a Laravel. Intente escribir cualquier aplicación PHP durante el soporte activo de las primeras versiones de PHP 7.0 y prepárese para pasar la noche buscando soluciones a los problemas que surgieron en módulos PHP obsoletos. Finalmente, un hecho interesante: el soporte para PHP 7.0 se suspendió antes que el soporte para PHP 5.6. Por un segundo
¿Qué hay de NodeJS? No diría que aquí todo es mucho mejor y que los períodos de soporte para NodeJS son fundamentalmente diferentes de PHP. No, es casi lo mismo aquí: cada versión LTS es compatible durante 3 años. Pero NodeJS tiene un poco más de estas versiones más actuales.

Si necesita implementar una aplicación escrita en 2016, asegúrese de que no tendrá absolutamente ningún problema con esto. Por cierto, la versión 6. * ya no será compatible solo en abril de este año. Y en frente hay 8, 10, 11 y los próximos 12.
Dificultades y sorpresas al cambiar a NodeJS
Comenzaré, tal vez, con la pregunta más emocionante para mí sobre cómo representar páginas HTML en NodeJS. Pero primero recordemos cómo se hace esto en PHP:
- Incruste HTML directamente en el código PHP. También lo hacen todos los novatos que aún no han llegado a MVC. Y así se hace en WordPress, lo cual es absolutamente horrible.
- Use MVC, que debería simplificar la interacción del desarrollador y proporcionar algún tipo de división del proyecto en partes, pero en realidad este enfoque solo complica todo a veces.
- Utiliza un motor de plantillas. La opción más conveniente, pero no en PHP. Solo mire la sintaxis sugerida en Twig or Blade con llaves y porcentajes.
Soy un ardiente oponente de combinar o fusionar varias tecnologías juntas. HTML debe existir por separado, los estilos para él por separado, JavaScript por separado (en React, esto generalmente parece monstruoso: HTML y JavaScript están mezclados). Es por eso que la opción ideal para desarrolladores con preferencias como la mía es un motor de plantillas. No tuve que buscarlo para una aplicación web en NodeJS durante mucho tiempo y opté por Jade (PugJS). Simplemente aprecie la simplicidad de su sintaxis:
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
Aquí todo es bastante simple: escribí una plantilla, la descargué en la aplicación, la compilé una vez y luego la utilicé en cualquier lugar conveniente en cualquier momento conveniente. En mi opinión, el rendimiento de PugJS es aproximadamente 2 veces mejor que la representación al incrustar HTML en el código PHP. Si anteriormente en PHP el servidor generó una página estática en aproximadamente 200-250 milisegundos, ahora esta vez es de aproximadamente 90-120 milisegundos (no estamos hablando de la representación en PugJS, sino del tiempo tomado desde la solicitud de la página hasta la respuesta del servidor al cliente con HTML listo) ) Así es como se ve la carga y compilación de plantillas y sus componentes en la etapa de inicio de la aplicación:
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) } } })
Parece increíblemente simple, pero con Jade hubo una pequeña complejidad en la etapa de trabajar con HTML ya compilado. El hecho es que para implementar scripts en la página, se utiliza una función asincrónica, que toma todos los archivos .js
del directorio y agrega la fecha de su último cambio a cada uno de ellos. La función tiene la siguiente forma:
for(let i = 0; i < files.length; i++) { let period = files[i].lastIndexOf('.')
En la salida, obtenemos una matriz de objetos con dos propiedades: la ruta al archivo y la hora en que se editó por última vez en la marca de tiempo (para actualizar el caché del cliente). El problema es que incluso en la etapa de recopilación de archivos de secuencia de comandos de un directorio, todos se cargan en la memoria estrictamente alfabéticamente (ya que se encuentran en el directorio en sí y los archivos se recopilan de arriba a abajo, desde el primero hasta el último). Esto condujo al hecho de que el archivo app.js se cargó al principio, y después vino el archivo core.min.js con polyfills y vendor.min.js al final. Este problema se resolvió de manera bastante simple: clasificación muy banal:
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 })
En PHP, todo tenía una apariencia monstruosa en forma de rutas a archivos JS previamente escritos en una cadena. Simple pero poco práctico.
NodeJS mantiene su aplicación en RAM
Esta es una gran ventaja. Todo está organizado para mí, de modo que en el servidor en paralelo e independientemente el uno del otro, hay dos sitios separados: la versión para el desarrollador y la versión de producción. Imagine que hice algunos cambios en los archivos PHP en el sitio de desarrollo y necesito implementar estos cambios en producción. Para hacer esto, debe detener el servidor o colocar un trozo de "lo siento, trabajo técnico" y, en este momento, copiar los archivos individualmente de la carpeta del desarrollador a la carpeta de producción. Esto provoca algún tipo de tiempo de inactividad y puede provocar la pérdida de conversiones. La ventaja de la aplicación en memoria en NodeJS es para mí que todos los cambios en los archivos del motor se realizarán solo después de que se reinicie. Esto es muy conveniente, porque puede copiar todos los archivos necesarios con los cambios y solo luego reiniciar el servidor. El proceso no lleva más de 1-2 segundos y no causa tiempo de inactividad.
El mismo enfoque se usa en nginx, por ejemplo. Primero edita la configuración, nginx -t
con nginx -t
y solo luego realice cambios con el service nginx reload
Agrupación de una aplicación NodeJS
NodeJS tiene una herramienta muy conveniente: el administrador de procesos pm2 . ¿Cómo solemos ejecutar aplicaciones en Node? Entramos en la consola y escribimos el node index.js
. Tan pronto como cerramos la consola, la aplicación se cierra. Al menos esto es lo que sucede en un servidor con Ubuntu. Para evitar esto y mantener la aplicación ejecutándose siempre, simplemente agréguela a pm2 con el simple comando de pm2 start index.js --name production
. Pero eso no es todo. La herramienta permite el monitoreo ( pm2 monit
) y la agrupación de aplicaciones.
Recordemos cómo se organizan los procesos en PHP. Supongamos que tenemos nginx sirviendo solicitudes http y necesitamos pasar la solicitud a PHP. Puede hacer esto directamente y luego con cada solicitud se generará un nuevo proceso PHP, y cuando se complete, se eliminará. O puede usar un servidor fastcgi. Creo que todos saben lo que es y no hay necesidad de entrar en detalles, pero por si acaso, aclararé que PHP-FPM se usa con mayor frecuencia como fastcgi y su tarea es generar muchos procesos PHP que están listos para aceptar y procesar una nueva solicitud en cualquier momento. ¿Cuál es la desventaja de este enfoque?
La primera es que nunca se sabe cuánta memoria consumirá su aplicación. En segundo lugar, siempre estará limitado en el número máximo de procesos y, en consecuencia, con un salto brusco en el tráfico, su aplicación PHP usará toda la memoria disponible y se bloqueará, o descansará contra el límite permitido de procesos y comenzará a eliminar los viejos. Esto se puede evitar estableciendo No recuerdo qué parámetro del archivo de configuración PHP-FPM es dinámico y luego se generarán tantos procesos como sea necesario en este momento. Pero de nuevo, un ataque DDoS elemental consumirá toda la RAM y pondrá su servidor. O, por ejemplo, un script de error consumirá toda la RAM y el servidor se congelará por algún tiempo (hubo precedentes en el proceso de desarrollo).
La diferencia fundamental en NodeJS es que la aplicación no puede consumir más de 1,5 gigabytes de RAM. No hay restricciones de proceso; solo hay un límite de memoria. Esto lo alienta a escribir programas lo más livianos posible. Además, es muy simple calcular la cantidad de clústeres que podemos permitirnos, dependiendo del recurso de CPU disponible. Se recomienda que no se cuelgue más de un clúster en cada núcleo (exactamente como en nginx, no más de un trabajador por núcleo de CPU).

Una ventaja de este enfoque es que PM2 recarga todos los clústeres a su vez. Volviendo al párrafo anterior, que hablaba del tiempo de inactividad de 1-2 segundos durante el reinicio. En el modo de clúster, cuando reinicia el servidor, su aplicación no experimentará un milisegundo de tiempo de inactividad.
NodeJS es un buen cuchillo suizo
Ahora existe tal situación cuando PHP actúa como un lenguaje para escribir sitios, y Python actúa como una herramienta para rastrear estos sitios. NodeJS es 2 en 1, por un lado es un tenedor, por el otro es una cuchara. Puede escribir aplicaciones y rastreadores web rápidos y potentes en el mismo servidor dentro de la misma aplicación. Suena tentador Pero, ¿cómo se puede realizar esto, preguntas? Google mismo lanzó la API oficial de Chromium: Puppeteer. Puede iniciar Headless Chrome (un navegador sin interfaz de usuario, Chrome "sin cabeza") y obtener el mayor acceso posible a la API del navegador para rastrear páginas. La forma más sencilla y accesible de trabajar con Puppeteer .
Por ejemplo, en nuestro grupo VKontakte hay una publicación regular de descuentos y ofertas especiales a varios destinos desde las ciudades de la CEI. Generamos imágenes para publicaciones en modo automático, y para que sean hermosas, necesitamos bellas imágenes. No me gusta vincularme a varias API y crear cuentas en docenas de sitios, por lo que escribí una aplicación simple que imita a un usuario común con el navegador Google Chrome que recorre el sitio con imágenes de archivo y recoge al azar la imagen encontrada por la palabra clave. Solía usar Python y BeautifulSoup para esto, pero ahora esto ya no es necesario. Y la característica principal y la ventaja de Puppeteer es que puede engañar fácilmente incluso a los sitios de SPA, porque tiene a su disposición un navegador completo que comprende y ejecuta el código JavaScript en los sitios. Es dolorosamente simple:
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' })
Entonces, en 3 líneas de código, lanzamos el navegador y abrimos la página del sitio con imágenes de archivo. Ahora podemos seleccionar un bloque aleatorio con la imagen en la página y agregarle una clase, en la que luego podemos acceder de la misma manera e ir directamente a la página con la imagen para cargar más:
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 })
Recuerde cuánto código se necesitaría para escribir esto en PhantomJS (que, por cierto, cerró y entró en estrecha colaboración con el equipo de desarrollo de Puppeteer). ¿Puede una herramienta tan maravillosa evitar que alguien cambie a NodeJS?
NodeJS proporciona asincronía fundamental
Esto puede considerarse una gran ventaja de NodeJS y JavaScript, especialmente con el advenimiento de async / wait en ES2017. A diferencia de PHP, donde cualquier llamada se realiza sincrónicamente. Daré un ejemplo simple. Anteriormente, en el motor de búsqueda, las páginas se generaban en el servidor, pero algo tenía que mostrarse en la página que ya estaba en el cliente usando JavaScript, pero en ese momento Yandex aún no podía usar JavaScript en los sitios web y tuvo que implementar un mecanismo de instantánea (instantáneas de página) específicamente para él. usando Prerender. Las instantáneas se almacenaron en nuestro servidor y se enviaron al robot a pedido. El dilema era que estas imágenes se generaban en 3-5 segundos, lo cual es completamente inaceptable y puede afectar la clasificación del sitio en los resultados de búsqueda. Para resolver este problema, se inventó un algoritmo simple: cuando el robot solicita alguna página, una instantánea de la que ya tenemos, simplemente le damos la instantánea existente, después de lo cual realizamos la operación para crear una nueva instantánea en segundo plano y reemplazarla. Ya disponible. Cómo se hizo en PHP:
exec('/usr/bin/php ' . __DIR__ . '/snapshot.php -a ' . $affiliation_type . ' -l ' . urlencode($full_uri) . ' > /dev/null 2>/dev/null &');
Nunca hagas eso.
En NodeJS, esto se puede lograr llamando a la función asincrónica:
async function saveSnapshot() { getSnapshot().then((res) => { db.saveSnapshot().then((status) => { if(status.err) console.error(err) }) }) } saveSnapshot()
En resumen, no está tratando de evitar el sincronismo, pero decide cuándo usar la ejecución de código síncrono y cuándo usarlo de forma asíncrona. Y es realmente conveniente. Especialmente cuando conoces las posibilidades de Promise.all ()
El motor de búsqueda de vuelos en sí está diseñado de tal manera que envía una solicitud a un segundo servidor que recopila y agrega datos, y luego recurre a él para obtener datos listos para emitir. Las páginas de dirección se utilizan para atraer tráfico orgánico.
Por ejemplo, para la consulta "Vuelos Moscú San Petersburgo" se emitirá una página con la dirección / tickets / moscow / saint-petersburg / , y necesita datos:
- Precios de las aerolíneas en esta dirección para el mes actual
- Precios de las aerolíneas en esta dirección para el próximo año (precio promedio de cada mes durante los próximos 12 meses)
- Programe vuelos en esta dirección
- Destinos populares desde la ciudad de despacho - desde Moscú (para vincular)
- Los destinos populares de la ciudad de llegada son desde San Petersburgo (para vincular)
En PHP, todas estas solicitudes se ejecutaron sincrónicamente, una tras otra. El tiempo promedio de respuesta de la API por solicitud es de 150-200 milisegundos. Multiplicamos 200 por 5 y obtenemos, en promedio, un segundo solo para cumplir con las solicitudes al servidor con datos. NodeJS tiene una gran función llamada Promise.all , que ejecuta todas las solicitudes en paralelo, pero escribe el resultado uno por uno. Por ejemplo, el código de ejecución para las cinco solicitudes anteriores se vería así:
var [montlyPrices, yearlyPrices, flightsSchedule, originPopulars, destPopulars] = await Promise.all([ getMontlyPrices(), getYearlyPrices(), getFlightSchedule(), getOriginPopulars(), getDestPopulars() ])
Y obtenemos todos los datos en 200-300 milisegundos, reduciendo el tiempo de generación de datos para la página de 1-1.5 segundos a ~ 500 milisegundos.
Conclusión
El cambio de PHP a NodeJS me ayudó a familiarizarme con JavaScript asincrónico, aprender a trabajar con promesas y async / wait. Después de que el motor fue reescrito, la velocidad de carga de la página se optimizó y difirió drásticamente de los resultados que mostró el motor en PHP. En este artículo, también podríamos hablar sobre cómo se usan los módulos simples para trabajar con el caché (Redis) y pg-promise (PostgreSQL) en NodeJS y compararlos con Memcached y php-pgsql, pero este artículo resultó ser bastante voluminoso. Y conociendo mi "talento" para escribir, también resultó estar mal estructurada. El propósito de este artículo es atraer la atención de los desarrolladores que todavía están trabajando con PHP y no están al tanto de las delicias de NodeJS y el desarrollo de aplicaciones basadas en la web utilizando un ejemplo de un proyecto de la vida real que alguna vez se escribió en PHP, pero debido a las preferencias su dueño fue a otra plataforma.
Espero haber podido transmitir mis pensamientos y más o menos estructurado para expresarlos en este material. Al menos lo intenté :)
Escriba cualquier comentario, amigable o enojado. Contestaré a cualquier constructivo.