ruby on rails - rails - ¿Qué es el middleware Rack?
ruby and rails documentation (7)
¿Qué es Rack?
Rack proporciona una interfaz mínima entre los servidores web que soportan los frameworks Ruby y Ruby.
Usando Rack puedes escribir una aplicación de Rack.
Rack pasará el hash del entorno (un Hash, contenido dentro de una solicitud HTTP de un cliente, que consta de encabezados CGI) a su aplicación de Rack, que puede usar las cosas contenidas en este hash para hacer lo que quiera.
¿Qué es una aplicación de rack?
Para usar Rack, debe proporcionar una ''aplicación'': un objeto que responde al método #call
con el Hash de entorno como un parámetro (normalmente definido como env
). #call
debe devolver una matriz de exactamente tres valores:
- el código de estado (por ejemplo, ''200''),
- un hash de encabezados ,
- el Cuerpo de Respuesta (que debe responder al método Ruby,
each
).
Puede escribir una aplicación de Rack que devuelva dicha matriz; esto se enviará de nuevo a su cliente, por Rack, dentro de una Respuesta (en realidad será una instancia de Class Rack::Response
[haga clic para ir a documentos]).
Una aplicación de rack muy simple:
-
gem install rack
- Cree un archivo
config.ru
- Rack sabe que debe buscar esto.
Crearemos una pequeña Aplicación de Rack que devuelve una Respuesta (una instancia de Rack::Response
) cuyo Cuerpo de Respuesta es una matriz que contiene una Cadena: "Hello, World!"
.
rackup
un servidor local usando el comando rackup
.
Cuando visitemos el puerto correspondiente en nuestro navegador, veremos "¡Hola, mundo!" prestados en la ventana gráfica.
#./message_app.rb
class MessageApp
def call(env)
[200, {}, [''Hello, World!'']]
end
end
#./config.ru
require_relative ''./message_app''
run MessageApp.new
rackup
un servidor local con rackup
y visite localhost: 9292 y debería ver ''¡Hola, mundo!'' rendido
Esta no es una explicación completa, pero básicamente lo que sucede aquí es que el Cliente (el navegador) envía una Solicitud de HTTP a Rack, a través de su servidor local, y Rack MessageApp
instancia de MessageApp
y ejecuta la call
, pasando el Hash del entorno como un parámetro a la Método (el argumento env
).
Rack toma el valor de retorno (la matriz) y lo usa para crear una instancia de Rack::Response
y lo envía de vuelta al Cliente. El navegador utiliza la magic para imprimir ''¡Hola, mundo!'' a la pantalla.
Por cierto, si desea ver cómo se ve el hash del entorno, simplemente coloque los puts env
debajo de la def call(env)
.
¡Mínimo como es, lo que has escrito aquí es una aplicación de Rack!
Hacer que una aplicación de rack interactúe con el hash del entorno entrante
En nuestra aplicación Little Rack, podemos interactuar con el hash env
(vea here para obtener más información sobre el hash del ambiente).
Implementaremos la capacidad para que el usuario ingrese su propia cadena de consulta en la URL, por lo tanto, esa cadena estará presente en la solicitud HTTP, encapsulada como un valor en uno de los pares clave / valor del hash del entorno.
Nuestra aplicación Rack accederá a esa cadena de consulta desde el hash del entorno y la enviará de vuelta al cliente (nuestro navegador, en este caso) a través del Cuerpo en la Respuesta.
De los documentos de Rack en el hash de entorno: "QUERY_STRING: la parte de la URL de solicitud que sigue al?, Si corresponde. Puede estar vacío, ¡pero siempre es necesario!"
#./message_app.rb
class MessageApp
def call(env)
message = env[''QUERY_STRING'']
[200, {}, [message]]
end
end
Ahora, rackup
y visite localhost:9292?hello
( ?hello
es la cadena de consulta) y debería ver "hola" en la ventana gráfica.
Rack Middleware
Lo haremos:
- inserte una pieza de Rack Middleware en nuestro código base - una clase:
MessageSetter
, - El hash del entorno afectará a esta clase primero y se pasará como parámetro:
env
, -
MessageSetter
insertará una clave''MESSAGE''
en el hash env, cuyo valor será''Hello, World!''
sienv[''QUERY_STRING'']
está vacío;env[''QUERY_STRING'']
si no, - finalmente, devolverá
@app.call(env)
-@app
es la siguiente aplicación en el ''Stack'':MessageApp
.
Primero, la versión ''mano larga'':
#./middleware/message_setter.rb
class MessageSetter
def initialize(app)
@app = app
end
def call(env)
if env[''QUERY_STRING''].empty?
env[''MESSAGE''] = ''Hello, World!''
else
env[''MESSAGE''] = env[''QUERY_STRING'']
end
@app.call(env)
end
end
#./message_app.rb (same as before)
class MessageApp
def call(env)
message = env[''QUERY_STRING'']
[200, {}, [message]]
end
end
#config.ru
require_relative ''./message_app''
require_relative ''./middleware/message_setter''
app = Rack::Builder.new do
use MessageSetter
run MessageApp.new
end
run app
De los documentos de Rack :: Builder vemos que Rack::Builder
implementa un DSL pequeño para construir iterativamente aplicaciones de Rack. Básicamente, esto significa que puede crear una ''Pila'' que consiste en uno o más Middlewares y una aplicación de ''nivel inferior'' para enviar. Todas las solicitudes que lleguen a su aplicación de nivel inferior serán procesadas primero por su Middleware (s).
#use
especifica middleware para usar en una pila. Toma el middleware como argumento.
Rack Middleware debe:
- tener un constructor que tome la siguiente aplicación en la pila como un parámetro.
- responda al método de
call
que toma el hash del entorno como parámetro.
En nuestro caso, el ''Middleware'' es MessageSetter
, el ''constructor'' es el método de initialize
de MessageSetter, la ''próxima aplicación'' en la pila es MessageApp
.
Así que aquí, debido a lo que Rack::Builder
hace bajo el capó, el argumento de app
del método de initialize
de MessageSetter
es MessageApp
.
(mueve tu cabeza alrededor de lo anterior antes de continuar)
Por lo tanto, cada pieza de Middleware esencialmente "pasa" el hash de Environment existente a la siguiente aplicación de la cadena, por lo que tiene la oportunidad de mutar ese hash de entorno en el Middleware antes de pasarlo a la siguiente aplicación en la pila.
#run
toma un argumento que es un objeto que responde a #call
y devuelve una Respuesta de Rack (una instancia de Rack::Response
).
Conclusiones
Usando Rack::Builder
, puede construir cadenas de Middlewares y cualquier solicitud a su aplicación será procesada por cada Middleware a su vez antes de que finalmente sea procesada por la pieza final en la pila (en nuestro caso, MessageApp
). Esto es extremadamente útil porque separa diferentes etapas de procesamiento de solicitudes. En términos de ''separación de preocupaciones'', ¡no podría ser mucho más limpio!
Puede construir un ''pipeline de solicitud'' que consiste en varios Middlewares que tratan con cosas tales como:
- Autenticación
- Autorización
- Almacenamiento en caché
- Decoración
- Monitoreo de rendimiento y uso
- Ejecución (en realidad manejar la solicitud y proporcionar una respuesta)
(por encima de las viñetas de otra respuesta en este hilo)
A menudo verás esto en aplicaciones profesionales de Sinatra. Sinatra utiliza Rack! ¡Vea here para la definición de lo que es Sinatra!
Como nota final, nuestro config.ru
se puede escribir en un estilo de mano corta, produciendo exactamente la misma funcionalidad (y esto es lo que normalmente verá):
require_relative ''./message_app''
require_relative ''./middleware/message_setter''
use MessageSetter
run MessageApp.new
Y para mostrar más explícitamente lo que está haciendo MessageApp
, aquí está su versión de "mano larga" que muestra explícitamente que #call
está creando una nueva instancia de Rack::Response
, con los tres argumentos necesarios.
class MessageApp
def call(env)
Rack::Response.new([env[''MESSAGE'']], 200, {})
end
end
Enlaces útiles
¿Qué es Rack middleware en Ruby? No pude encontrar ninguna buena explicación de lo que quieren decir con "middleware".
Rack como diseño
El middleware de Rack es más que "una forma de filtrar una solicitud y una respuesta", es una implementación del patrón de diseño de canalización para servidores web que usan Rack .
Separa de manera muy clara las diferentes etapas del procesamiento de una solicitud: la separación de preocupaciones es un objetivo clave de todos los productos de software bien diseñados.
Por ejemplo, con Rack puedo tener etapas separadas de la tubería haciendo:
Autenticación : cuando llega la solicitud, ¿son correctos los detalles de inicio de sesión de los usuarios? ¿Cómo valido este OAuth, autenticación básica HTTP, nombre / contraseña?
Autorización : "¿el usuario está autorizado para realizar esta tarea en particular?", Es decir, la seguridad basada en roles.
Almacenamiento en caché : ¿ya procesé esta solicitud? ¿Puedo devolver un resultado en caché?
Decoración : ¿cómo puedo mejorar la solicitud para mejorar el procesamiento posterior?
Monitoreo de rendimiento y uso : ¿qué estadísticas puedo obtener de la solicitud y respuesta?
Ejecución : en realidad manejar la solicitud y proporcionar una respuesta.
Ser capaz de separar las diferentes etapas (y, opcionalmente, incluirlas) es una gran ayuda para desarrollar aplicaciones bien estructuradas.
Comunidad
También hay un gran ecosistema que se desarrolla en torno al Rack Middleware: debería poder encontrar componentes de rack preconstruidos para realizar todos los pasos anteriores y mucho más. Consulte la wiki de Rack GitHub para obtener una lista de middleware .
¿Qué es el middleware?
Middleware es un término terrible que se refiere a cualquier componente / biblioteca de software que ayuda con, pero no está directamente involucrado en la ejecución de alguna tarea. Algunos ejemplos muy comunes son el registro, la autenticación y los otros componentes comunes de procesamiento horizontal . Estas tienden a ser las cosas que todos necesitan en múltiples aplicaciones, pero no muchas personas están interesadas (o deberían estar) en construirse.
Más información
El comentario acerca de que se trata de una forma de filtrar solicitudes probablemente proviene del episodio 151 de RailsCast: pantalla de Rack Middleware .
Rack middleware evolucionó a partir de Rack y hay una excelente introducción en Introduction to Rack middleware .
Hay una introducción al middleware en Wikipedia here .
El middleware de rack es una forma de filtrar una solicitud y respuesta que llega a su aplicación. Un componente de middleware se encuentra entre el cliente y el servidor, procesando solicitudes entrantes y respuestas salientes, pero es más que una interfaz que se puede usar para hablar con el servidor web. Se utiliza para agrupar y ordenar módulos, que normalmente son clases de Ruby, y especificar la dependencia entre ellos. El módulo de middleware de rack solo debe: - tener un constructor que tome la siguiente aplicación en la pila como parámetro - responder al método de "llamada", que tome el hash del entorno como un parámetro. El valor de retorno de esta llamada es una matriz de: código de estado, hash del entorno y cuerpo de respuesta.
En primer lugar, Rack es exactamente dos cosas:
- Una convención de interfaz de servidor web.
- Una gema
Rack - La interfaz del servidor web
Lo más básico del rack es una simple convención. Cada servidor web compatible con rack siempre invocará un método de llamada en un objeto que le des y le proporcionará el resultado de ese método. Rack especifica exactamente cómo debe verse este método de llamada, y qué tiene que devolver. Eso es rack.
Vamos a darle un intento simple. Usaré WEBrick como servidor web compatible con rack, pero cualquiera de ellos servirá. Vamos a crear una aplicación web simple que devuelve una cadena JSON. Para esto vamos a crear un archivo llamado config.ru. El comando rackup de rack gem llamará automáticamente a config.ru, que simplemente ejecutará el contenido de config.ru en un servidor web compatible con rack. Así que vamos a agregar lo siguiente al archivo config.ru:
class JSONServer
def call(env)
[200, {"Content-Type" => "application/json"}, [''{ "message" : "Hello!" }'']]
end
end
map ''/hello.json'' do
run JSONServer.new
end
Como la convención especifica, nuestro servidor tiene un método llamado llamada que acepta un hash de entorno y devuelve una matriz con el formulario [estado, encabezados, cuerpo] para que el servidor web sirva. Vamos a probarlo simplemente llamando rackup. Un servidor predeterminado compatible con el bastidor, tal vez WEBrick o Mongrel se iniciará y esperará inmediatamente a que se atiendan las solicitudes.
$ rackup
[2012-02-19 22:39:26] INFO WEBrick 1.3.1
[2012-02-19 22:39:26] INFO ruby 1.9.3 (2012-01-17) [x86_64-darwin11.2.0]
[2012-02-19 22:39:26] INFO WEBrick::HTTPServer#start: pid=16121 port=9292
Probemos nuestro nuevo servidor JSON curling o visitando la url http://localhost:9292/hello.json
y voila:
$ curl http://localhost:9292/hello.json
{ message: "Hello!" }
Funciona. ¡Genial! Esa es la base para cada framework web, ya sea Rails o Sinatra. En algún punto, implementan un método de llamada, trabajan a través de todo el código del marco y finalmente devuelven una respuesta en la forma típica [estado, encabezados, cuerpo].
En Ruby on Rails, por ejemplo, las solicitudes de rack ActionDispatch::Routing.Mapper
clase ActionDispatch::Routing.Mapper
que se ve así:
module ActionDispatch
module Routing
class Mapper
...
def initialize(app, constraints, request)
@app, @constraints, @request = app, constraints, request
end
def matches?(env)
req = @request.new(env)
...
return true
end
def call(env)
matches?(env) ? @app.call(env) : [ 404, {''X-Cascade'' => ''pass''}, [] ]
end
...
end
end
Básicamente, los cheques de Rails dependen del env hash si alguna ruta coincide. Si es así, pasa el hash env a la aplicación para calcular la respuesta; de lo contrario, responde de inmediato con un 404. Así, cualquier servidor web que cumpla con la convención de interfaz de rack puede servir una aplicación Rails completamente desarrollada.
Middleware
Rack también admite la creación de capas de middleware. Básicamente, interceptan una solicitud, hacen algo con ella y la pasan. Esto es muy útil para tareas versátiles.
Digamos que queremos agregar el registro a nuestro servidor JSON que también mide el tiempo que tarda una solicitud. Simplemente podemos crear un registrador de middleware que haga exactamente esto:
class RackLogger
def initialize(app)
@app = app
end
def call(env)
@start = Time.now
@status, @headers, @body = @app.call(env)
@duration = ((Time.now - @start).to_f * 1000).round(2)
puts "#{env[''REQUEST_METHOD'']} #{env[''REQUEST_PATH'']} - Took: #{@duration} ms"
[@status, @headers, @body]
end
end
Cuando se crea, se guarda una copia de la aplicación de rack real. En nuestro caso es una instancia de nuestro JSONServer. Rack llama automáticamente al método de llamada en el middleware y espera una matriz de [status, headers, body]
, tal como lo hace nuestro JSONServer.
Entonces, en este middleware, se toma el punto de inicio, luego la llamada real al JSONServer se realiza con @app.call(env)
, luego el registrador genera la entrada de registro y finalmente devuelve la respuesta como [@status, @headers, @body]
.
Para hacer que nuestro pequeño rackup.ru use este middleware, agregue un uso de RackLogger a este:
class JSONServer
def call(env)
[200, {"Content-Type" => "application/json"}, [''{ "message" : "Hello!" }'']]
end
end
class RackLogger
def initialize(app)
@app = app
end
def call(env)
@start = Time.now
@status, @headers, @body = @app.call(env)
@duration = ((Time.now - @start).to_f * 1000).round(2)
puts "#{env[''REQUEST_METHOD'']} #{env[''REQUEST_PATH'']} - Took: #{@duration} ms"
[@status, @headers, @body]
end
end
use RackLogger
map ''/hello.json'' do
run JSONServer.new
end
Reinicie el servidor y listo, genera un registro en cada solicitud. Rack le permite agregar múltiples middlewares que se llaman en el orden en que se agregan. Es solo una excelente manera de agregar funcionalidad sin cambiar el núcleo de la aplicación de rack.
Rack - La Gema
Aunque el bastidor, en primer lugar, es una convención, también es una joya que proporciona una gran funcionalidad. Uno de ellos ya lo usamos para nuestro servidor JSON, el comando rackup. ¡Pero hay más! La gema de rack proporciona pequeñas aplicaciones para muchos casos de uso, como servir archivos estáticos o incluso directorios completos. Veamos cómo servimos un archivo simple, por ejemplo, un archivo HTML muy básico ubicado en htmls / index.html:
<!DOCTYPE HTML>
<html>
<head>
<title>The Index</title>
</head>
<body>
<p>Index Page</p>
</body>
</html>
Tal vez deseamos servir este archivo desde la raíz del sitio web, así que agreguemos lo siguiente a nuestro config.ru:
map ''/'' do
run Rack::File.new "htmls/index.html"
end
Si visitamos http://localhost:9292
, vemos nuestro archivo html perfectamente renderizado. Eso fue fácil, ¿verdad?
Agreguemos un directorio completo de archivos javascript creando algunos archivos javascript en / javascripts y agregando lo siguiente a config.ru:
map ''/javascripts'' do
run Rack::Directory.new "javascripts"
end
Reinicie el servidor y visite http://localhost:9292/javascript
y verá una lista de todos los archivos javascript que puede incluir ahora directamente desde cualquier lugar.
He usado Rack middleware para resolver un par de problemas:
- Capturar los errores de análisis JSON con el middleware de Rack personalizado y devolver mensajes de error bien formateados cuando el cliente envía JSON bloqueado
- Compresión de contenido a través de Rack :: Deflater
En ambos casos proporcionó soluciones bastante elegantes.
Tuve problemas para entender a Rack por una buena cantidad de tiempo. Solo lo entendí completamente después de trabajar en hacer este servidor web en miniatura de Ruby . He compartido mis aprendizajes sobre Rack (en forma de una historia) aquí en mi blog: http://gauravchande.com/what-is-rack-in-ruby-rails
La retroalimentación es más que bienvenida.
config.ru
ejemplo ejecutable mínimo
app = Proc.new do |env|
[
200,
{
''Content-Type'' => ''text/plain''
},
["main/n"]
]
end
class Middleware
def initialize(app)
@app = app
end
def call(env)
@status, @headers, @body = @app.call(env)
[@status, @headers, @body << "Middleware/n"]
end
end
use(Middleware)
run(app)
Ejecutar rackup
y visita localhost:9292
. La salida es:
main
Middleware
Así que está claro que el Middleware
envuelve y llama a la aplicación principal. Por lo tanto, puede preprocesar la solicitud y postprocesar la respuesta de cualquier manera.
Como se explica en: http://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack , Rails usa Rack Middlewares para muchas de sus funciones, y también puede agregar su propia propiedad con config.middleware.use
Métodos familiares.
La ventaja de implementar la funcionalidad en un middleware es que puede reutilizarlo en cualquier marco de Rack, por lo tanto, todos los principales Ruby, y no solo Rails.