Aprenda HTTP/2 Server Push construyendo Express Middleware

En la publicación anterior, aprendimos cómo realizar una inserción de servidor HTTP/2 en un servidor Node. También cubrimos los beneficios de la inserción del servidor allí, por lo que para evitar la duplicación no los enumeraremos aquí. Usamos spdy para servidor push y H2. Pero la mayoría de las veces, los desarrolladores de Node no trabajan con el servidor HTTP central, usan un marco como Express. Entonces, veamos cómo podemos implementar la inserción del servidor en Express.

Para ilustrar el empuje del servidor HTTP/2 con Express, implementaremos un middleware Express que enviará cualquier imagen o secuencia de comandos al navegador. El middleware utilizará un mapa hash de dependencias, por así decirlo. Por ejemplo, index.html tendrá bundle.js , node-university-animation.gif imagen y un script más bundle2.js .

Incluso puede usar este middleware para servir imágenes. La expresión regular funcionará sin modificaciones porque tanto <script> y <img> las etiquetas usan el atributo src. Así es como se verá empujando una imagen (de la animación de Node.University):

Como puede ver, con la imagen tampoco hay una barra verde (Esperando TTFB).

Nota:Este middleware no está diseñado para uso en producción. Su propósito es ilustrar lo que es posible en el protocolo HTTP/2 y Node+Express.

Estructura del proyecto

El código del proyecto está en GitHub y la estructura del proyecto es un servidor Express típico con una carpeta estática:

/node_modules
/public
  - bundle.js
  - bundle2.js
  - index.html
  - node-university-animation.gif
- index-advanced.js
- package.json
- server.crt
- server.csr
- server.key

No comprometí claves SSL por razones obvias (¡tampoco debería hacerlo usted en sus proyectos!), así que genere las suyas propias. HTTP/2 no funcionará sin SSL/HTTPS. Puede obtener las instrucciones en Optimizar su aplicación con HTTP/2 Server Push usando Node y Express o Servidor HTTP/2 sencillo con Node.js y Express.js .

Instalación de dependencias

En primer lugar, declara las dependencias en tu package.json con estas dependencias de npm:

{
  "name": "http2-node-server-push",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "./node_modules/.bin/node-dev .",
    "start-advanced": "./node_modules/.bin/node-dev index-advanced.js"
  },
  "keywords": [
    "node.js",
    "http2"
  ],
  "author": "Azat Mardan",
  "license": "MIT",
  "dependencies": {
    "express": "^4.14.0",
    "morgan": "^1.7.0",
    "spdy": "^3.4.0"
  },
  "devDependencies": {
    "node-dev": "^3.1.3"
  }
}

Siéntase libre de copiar package.json y ejecuta npm i .

Archivo HTML

El index.html tiene tres activos:

<html>
<body>
  <script src="bundle.js"/></script>

  <h1>hello to http2 push server!</h1>
  <div></div>

  <img src="node-university-animation.gif"/>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
</body>
  <script src="bundle2.js"/></script>
</html>

bundle.js es muy pequeño:

console.log('bundle1')

Por otro lado, bundle2.js es bastante grande (tiene núcleo React).

Definición de Servidor Express

Echemos un vistazo a la implementación que está en el index-advanced.js . Al principio, definimos las dependencias como Express y algunos otros módulos. El pushOps el objeto se usará más tarde para

[Nota al margen]

Leer publicaciones de blog es bueno, pero ver cursos en video es aún mejor porque son más atractivos.

Muchos desarrolladores se quejaron de la falta de material de video de calidad asequible en Node. Es una distracción ver videos de YouTube y una locura pagar $ 500 por un curso de video de Node.

Visite Node University, que tiene cursos de video GRATUITOS en Node:node.university.

[Fin de la nota al margen]

var express = require('express')
var app = express()
const fs = require('fs')
const path = require('path')
const url = require('url')

Ahora, leamos y mapeemos todo el script y la imagen incluidos en todos los archivos usando este algoritmo. Se ejecutará solo una vez cuando inicie el servidor, por lo que no consumirá tiempo durante las solicitudes. Está bien usar un readFileSync porque aún no estamos ejecutando el servidor.

let files = {}
fs.readdir('public', (error, data)=>{
  data.forEach(name=>{
    files[`${name}`]=fs
      .readFileSync(path.join(__dirname, 'public', `${name}`), {encoding: 'utf8'})
      .split('\n')
      .filter(line=>line.match(/src *?= *?"(.*)"/)!=null)
      .map(line=>line.match(/src *?= *?"(.*)"/)[1])
  })
})

Las funciones dentro del filter y map utilizará la expresión regular para producir este objeto:

{ 'bundle.js': [],
  'bundle2.js': [],
  'index.html': [ 'bundle.js', 'node-university-animation.gif', 'bundle2.js' ],
  'node-university-animation.gif': [] }

Usando index.html como clave de este objeto, podremos acceder rápidamente a la matriz de sus dependencias. Una matriz vacía significa que no hay dependencias que podamos insertar en el servidor.

A continuación, defina el middleware de registro para realizar un seguimiento de las solicitudes en el lado del servidor:

const logger = require('morgan')
app.use(logger('dev'))

Implementación de middleware de inserción de servidor

Entonces obtuvimos el objeto que tiene información sobre qué empujar. Para impulsar realmente los activos, cree un middleware como este en el que eliminamos el / y predeterminado a index.html cuando no hay una ruta en la URL (como para https://localhost:8080/ el urlName se convertirá en index.html ):

app.use((request, response, next)=>{
  let urlName = url.parse(request.url).pathname.substr(1)
  if (urlName === '' || urlName === '/') urlName = 'index.html'
  console.log('Request for: ', urlName)

Por supuesto, vamos a comprobar si tenemos este archivo en nuestro public carpeta haciendo coincidir el nombre como una clave del files objeto. Si es verdadero, continúe y cree assets para almacenar el código para la inserción del servidor. Cada assets el elemento de la matriz será un activo como un script o una imagen.

  if (files[urlName]) {
    let assets = files[urlName]
      .filter(name=>(name.substr(0,4)!='http'))
      .map((fileToPush)=>{
        let fileToPushPath = path.join(__dirname, 'public', fileToPush)
        return (cb)=>{
          fs.readFile(fileToPushPath, (error, data)=>{
            if (error) return cb(error)
            console.log('Will push: ', fileToPush, fileToPushPath)
            try {
              response.push(`/${fileToPush}`, {}).end(data)
              cb()
            } catch(e) {
              cb(e)
            }
          })
        }
      })

El empuje real está ocurriendo en response.push( /${fileToPush}, {}).end(data) . Puede mejorar esta llamada pasando el tipo de contenido en lugar del objeto vacío {} . Además, es posible usar flujo y no un búfer data de readFile .

A continuación, agreguemos el index.html mismo (o cualquiera que sea el nombre del archivo):

    // Uncomment to disable server push
    // assets = []
    console.log('Total number of assets to push: ', assets.length)
    assets.unshift((cb)=>{
      fs.readFile(path.join(__dirname, 'public', urlName), (error, data)=>{
        if (error) return cb(error)
        response.write(data)
        cb()
      })
    })

Ahora, podemos enviar todos los activos y HMTL de una sola vez:

    require('neo-async').parallel(assets, (results)=>{
      response.end()
    })
  } else {
    return next()
  }
})

Lanzamiento del servidor HTTP/2

Finalmente, inicie el servidor H2 usando claves, certificado y spdy :

var options = {
  key: fs.readFileSync('./server.key'),
  cert: fs.readFileSync('./server.crt')
}

require('spdy')
  .createServer(options, app)
  .listen(8080, ()=>{
    console.log(`Server is listening on https://localhost:8080.
You can open the URL in the browser.`)
  }
)

Cuando inicie el servidor con npm run start-advanced , luego verá este aviso:

Server is listening on https://localhost:8080.
You can open the URL in the browser.

Solo recuerda usar https y no http. Si bien según el estándar HTTP/2 es posible usar el protocolo http sin cifrar, la mayoría de los navegadores decidieron admitir solo https por razones obvias de seguridad.

Al realizar una solicitud a la página de inicio, el servidor enviará index.html . Como puede ver en los registros, solo hay una solicitud cuando se usa la inserción del servidor.

Request for:  index.html
Total number of assets to push:  13
Will push:  bundle.js /Users/azat/Documents/Code/http2-node-server-push/public/bundle.js
Will push:  node-university-animation.gif /Users/azat/Documents/Code/http2-node-server-push/public/node-university-animation.gif
Will push:  bundle2.js /Users/azat/Documents/Code/http2-node-server-push/public/bundle2.js

Hemos terminado con nuestro servidor y middleware. Inicie el servidor y vea los resultados en https://localhost:8080/. Pueden variar...

Resumen

Los beneficios reales de una inserción de servidor dependen de muchos factores, como el almacenamiento en caché, el orden de los activos, el tamaño y la complejidad de la representación de HTML. No obtuve mucho impulso en mi index.html , pero el "TTFB en espera" desaparece con las pulsaciones H2.

Puedes jugar descomentando assets = [] que básicamente elimina el código de inserción de activos. Lo interesante es que obtuve la hora de inicio (pestaña Red en DevTools) de los activos más rápido que otros con HTTP/2 Server Push:

Mientras que sin empujar, el orden de inicio SIEMPRE será el mismo que en HTML , es decir, bundle.js , node-university-animation.gif y bundle2.js .

Server push es extremadamente poderoso, pero debe usarse con conocimiento para evitar cualquier conflicto con el almacenamiento en caché, por ejemplo, enviar activos que ya están en caché. Los resultados de Server Push dependen de muchos factores. Puede utilizar este middleware con fines educativos. Si te gustó este artículo, considera revisar Node.University.