fpWeb Tutorial/es

From Free Pascal wiki
Jump to navigationJump to search

العربية (ar) English (en) español (es) русский (ru)

Originalmente basado en el tutorial de fcl-web (en PDF) por un usuario del foro Leledumbo.

Introducción

fpWeb es un esquema de aplicación web que viene con FPC por defecto como parte del paquete fcl-web. El esquema en sí mismo está construido encima de las características de fcl-web. El esquema está construido con mentalidad de RAD para hacer buen uso de la componentización en producir contenido dinámico. Un paquete Lazarus es provisto que pueda usar el esquema en forma de arrastrar y soltar para su administración de sesión y producción de contenido.

Este tutorial intentará cubrir la funcionalidad básica de fpWeb, así que uno puede construir una aplicación común para web con él. Note que este tutorial NO intenta enseñar ningún protocolo HTTP, HTML, CSS, JavaScript o manipulación de base de datos como los lenguajes de protocolo y cliente debieran ser los prerequisitos para cada programador de aplicaciones web y la manipulación de base de datos no difiere de la implementación de escritorio.

Arquitectura (lea POR FAVOR)

Antes de iniciar, es necesario conocer la arquitectura y flujo de aplicación para evitar confusiones cuando ciertas cosas no funcionen o lo hagan de forma inesperada. Así que por favor pase algo de tiempo leyendo esta sección.

Aplicación

La aplicación aquí se refiere al protocolo que su aplicación implementará. fpWeb felizmente se cambia de módulos CGI, FCGI, Apache a un servidor embebido, y más si fcl-web implementa otro en el futuro. Cada aplicación se implementa en su propia unidad, así que para cambiar de una aplicación a otra, con la excepción del módulo Apache, sólo se necesita cambiar el respectivo identificador en la cláusula uses. Para 3.0.0 en adelante son:

  • fpCGI -> CGI
  • fpFCGI -> FastCGI
  • fpApache (requiere httpd también) -> módulo Apache
  • fpHttpApp -> servidor embebido
  • microhttpapp -> servidor embebido usando la biblioteca GNU libmicrohttp.
  • fphttpsys -> soporte de sistema Windows para el protocolo HTTP.

A través de este tutorial, usaremos el servidor embebido en beneficio de simplicidad porque no tiene que tratar con configurar el servidor virtual ni enredarse con el manejo complicado de archivos y servicios. Su aplicación será una aplicación portátil binaria para web! Otra razón sería que hay más de una aplicación de servidor web y que cada una tiene una forma distinta de configurar. Sería exagerado cubrir todos mientras que su documentación hace el trabajo. El módulo Apache se implementa como una biblioteca (dinámica), mientras que otros protocolos son aplicaciones normales. Cada aplicación puede que tenga propiedades específicas (como puertos) disponibles y que sólo competen a esa aplicación. Eso es el porqué de ver los ejemplos fcl-web, pares de .lpi / .lpr para cada protocolo están puestos en sus propios directorios, sólo los módulos web se comparten.


Módulos Web

fpWeb-overview.png

Las aplicaciones fpWeb constan de módulos web los que hacen la producción actual de contenido. Un módulo web puede contener acciones web que pueden dividir la funcionalidad incluso más. Por ejemplo, un módulo web auténtico puede que tenga acciones web de login y logout. Mientras que un módulo web de acerca de, puede no necesitar una acción específica y sirve sólo para un contenido. El módulo Web se integra con fpTemplate que puede usarse para producir contenido dinámico con una plantilla. Esto es bastante similar a lo que hace PHP, sólo el espacio entre la lógica y la presentación se impone en vez de lo que se sugiere. Algunos dicen que fpTemplate implementa una vista pasiva mientras que PHP por defecto implementa un patrón de diseño de vista activa.


Instalación

El paquete fpWeb para Lazarus no se instala por defecto (pero viene con él), para activar fpWeb:

  1. Abra Lazarus y escoja Paquetes->Instalar/Desinstalar Paquetes’’’
  2. En el listbox disponible para la instalación, busque weblaz y presione Instala selección. Presione Guardar y reconstruir IDE y confirme con Continuar
  3. Deje que el IDE se reconstruya y reincide solo. Si todo va bien, deberá ahora tener una pestaña ftweb en la paleta de componentes, como se muestra:
Installed weblaz package

Módulos Especializados

La clase *TFPWebModule* (usada abajo) es un simple ejemplo del módulo fpWEB que puede usarse para toda clase de peticiones HTTP.

Sin embargo, fpWEB viene con algunos módulos especializados que tienen soporte extra para tareas especializadas:

  • La clase TSimpleFileModule en la unidad fpwebfile.pp puede usarse para enviar archivos. Sólo apunte a un directorio, y él hace el resto.
  • La clase TFPHTMLModule en la unidad fphtml.pp puede usarse para producir HTML.
  • La clase TProxyWebModule en la unidad fpwebproxy.pp es un proxy de redirección listo para usar.
  • La clase TFPWebProviderDataModule en la unidad fpwebdata.pp sirve de datos en formato JSON que puede guardar ExtJS.
  • La clase TSQLDBRestModule en la unidad sqldbrestmodule.pp implementa un servidor REST completo respaldado por SQLDB. Vea más información en SQLDBRestBridge.
  • La clase TJSONRPCModule en la unidad webjsonrpc.pp implementa un servicio JSON-RPC.
  • La clase TExtDirectModule en la unidad fpextdirect.pp implementa una variante Ext.Direct de un servicio JSON-RPC.

Hola, Mundo!

Creemos una aplicación Web sencilla. Como se enseñó comúnmente cuando se aprendía programación, "Hola, Mundo!" será nuestra primera app.

1. Abra Lazarus y escoja Proyecto->Nuevo Proyecto luego HTTP server Application

Create new HTTP server application

2. Otro diálogo deberá aparecer para servir archivos estáticos, selección de puerto y multihilo. Sólo use por defecto el puerto 8080.

Puede que omita servir archivos estáticos (vaya a la sección de trucos y consejos si quiere saber más).
Static files, port selection and multithreading options


IMPORTANTE!:
Si escogió usar hilos en *nix, no olvide agregar cthreads como la primera unidad en la cláusula uses del archivo .lpr, de otra forma se generará un error RTE 232. Cuando se ejecuta desde una consola, un mensaje deberá aparecer:
Este binario no se compiló con soporte de hilos.  Recompile la aplicación con un manejado de hilasen la cláusula uses del programa antes de cualquier unidad que use hilos.


3. Desde Enero 14 de 2017 (o FPC 3.0.4), pueda que necesite abrir el archivo .lpr y agregar la siguiente línea en el cuerpo principal si no está puesto:

Application.LegacyRouting := true;
la razón estará explicada en el capítulo #Enrutamiento.


4. Cualquier cosa que escoja, clique "OK" y se le presentará en el módulo de aplicación fpWeb por defecto.

5. Enfoque el módulo y muévase al Inspector de Objetos. Siéntase libre de renombre el módulo si así lo prefiere.

6. Escoja la pestaña Events y clique el botón a la derecha de la segunda columna de la fila OnRequest para crear el manejador del evento.

Creating web module's OnRequest handler in the object inspector
Será dirigido al editor de código fuente con el código:
procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
begin
  |
end;
Escriba:
procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
begin
  AResponse.Content := 'Hola, Mundo!';
  Handled := true;
end;

7. Luego ejecute su aplicación (o presione F9).

8. Abra su navegador y escriba:

http://localhost:8080/

9. Deberá ver "Hola, Mundo!'.

En caso contrario, revise:

  • El marco de trabajo hace mucho de manejo de excepciones y el depurador del IDE podrá atraparlos e interrumpe su aplicación. Está bien agregar muchas de las excepciones para la lista del olvido así que se pueda concentrar más en su flujo de aplicación. Manténgase evitando y continuando hasta que no aparezcan más diálogos y el navegador muestre el contenido.
  • Handled := true es la forma que le decimos que la solicitud ha sido manejada. Si no se coloca (o se pone en false) mostrará la página de error. Por ahora, esto no afecta el flujo de peticiones, pero lo hará después. Así que déjelo así hasta que el momento en que se le de buen uso.
  • otra pista: pruebe sin un cortafuegos cargado en RAM (como una aplicación, como un servicio o daemon, o ambos).


Leyendo los datos GET & POST

Es probable que un contenido dinámico se active a partir de la entrada del usuario, tanto a través de formularios, proveyendo valores en el URL, etc. Esos datos se envían a través de la petición, la cual se representa en el método como ARequest parámetro de tipo TRequest.

Leyendo GET

Los datos GET se proveen como ARequest.QueryFields , el cual desciende de TStrings. Resumiendo, cualquier cosa que haga con TStrings, se aplica aquí como acceder a los datos en un estilo de mapa a través de la propiedad Values.

Reusando el código anterior, reemplace el cuerpo del método con:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
var
  LName: String;
begin
  LName := ARequest.QueryFields.Values['Name']; // QueryFields para GET
  if LName = EmptyStr then
    with AResponse.Contents do begin       // En el formulario <form> el
      Add('<form action="%s" method="GET"', [aRequest.URI]);// solicitante
      Add('<label for="name">Por favor dame tu nombre:</label>');// pondrá
      Add('<input type="text" name="name" id="name" />');// name="name"
      Add('<input type="submit" value="Send" />');
      Add('</form>');
    end
  else
    AResponse.Content := Format('<h1>Hola, %s!</h1>', [LName]);
  Handled := true;  // <— Se dio buen manejo!
end;

ARequest.URI es sólo una conveniencia referirse al URI actual, así que cuando cambie su módulo registrado, este código permanece igual.

Note que como en Pascal, nos referimos a los datos se hace sin importar las mayúsculas/minúsculas.

Como es una petición GET, en la dirección de la página aparece la sintaxis de lo solicitado. Ahora puede intentar la solicitud /, la cual mostrará

 Por favor dame tu nombre:

y al final de la dirección coloque /?name=<escriba algo aquí, p.e.: Fernando>, lo que mostrará

 Hola, Fernando!

Leyendo POST

POST no difiere mucho de GET, sólo en la propiedad que accede. Si GET se acceda a través de ARequest.QueryFields, POST se accede a través de ARequest.ContentFields. El estilo de POST del código previo es:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
var
  LName: String;
begin
  LName := ARequest.ContentFields.Values['Name']; // ContentFields para POST
  if LName = EmptyStr then
    with AResponse.Contents do
    begin
      Add('<form action="%d" method="POST"', [ARequest.URI]);
      Add('<label for="name">Por favor dame tu nombre:</label>');
      Add('<input type="text" name="name" id="name" />');
      Add('<input type="submit" value="Send" />');
      Add('</form>');
    end
  else
    AResponse.Content := Format('<h1>Hola, %s!</h1>', [LName]);
  Handled := true;
end;

Leyendo Subir Archivos

Una excepción es para los archivos de lectura multipart/form-data, p.e. los archivos. Eso está disponible en ARequest.Files como una instancia de TUploadedFiles, la cual desciende de TCollection. Lo siguiente es la interfaz pública TUploadedFiles la cual puede usarse para acceder a los archivos:

TUploadedFiles = Class(TCollection)
    ·
    ·
    ·
public
  Function First: TUploadedFile;
  Function Last: TUploadedFile;
  Function IndexOfFile(AName: String) : Integer;
  Function FileByName(AName: String) : TUploadedFile;
  Function FindFile(AName: String) : TUploadedFile;
  Property Files[Index: Integer] : TUploadedFile read GetFile Write SetFile; default;
end;

// Cada TUploadedFile por sí mismo tiene varias propiedades:

TUploadedFile = Class(TCollectionItem)
    ·
    ·
    ·
Public
  Destructor Destroy; override;
  Property FieldName: String Read FFieldName Write FFieldName;
  Property FileName: String Read FFileName Write FFileName;
  Property Stream: TStream Read GetStream;
  Property Size: Int64 Read FSize Write FSize;
  Property ContentType: String Read FContentType Write FContentType;
  Property Disposition: String Read FDisposition Write FDisposition;
  Property LocalFileName: String Read FLocalFileName Write FLocalFileName;
  Property Description: String Read FDescription Write FDescription;
end;

Debería ser lo suficientemente descriptivo, con la excepción de FileName y LocalFileName. FileName es el archivo original name como se subió desde el cliente, LocalFileName es la ruta del archivo en el servidor donde el archivo se guarda de forma temporal. Note la diferencia en los términos en negrita antes descritos.

Nuevamente, reusemos el mismo manejador de petición, nótese que se puede optimizar el código usando una constante Req:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
const
  Req = '<form id="form" action="%d" method=POST enctype="multipart' + 
	'/form-data"><label for="name">Drag n drop or click to' +
	' add file:</label><input type="file" name="input" />' +
	'<input type="submit" value="Send" /></form>';
var
  n, i: Integer;
  f: TUploadedFile;
begin
  n := ARequest.Files.Count;
  if n = 0 then
    with AResponse.Contents do 
      Add(Req, [ARequest.URI])
  else begin
    f := ARequest.Files[0];
    AResponse.Contents.LoadFromStream(f.Stream);
  end;
  Handled := true;
end;

arrastrar y soltar un archivo (preferiblemente texto, tal si fuese presentado como texto) en el campo de entrada de archivo (o clique el botón respectivo) entonces clique en el botón Enviar. El contenido de archivo deberá mostrarse.

Cookies

Configuración

El concepto de "cookie" (por el monstruo comegalletas?), inventado por Netscape en 1994, para permitir al servidor HTTP identificar a todos sus clientes.

El guardar y mantener las cookies son responsabilidad del navegador, por tanto el servidor necesita enviarlo como parte de la respuesta para mantener una. AResponse.Cookies contiene una lista de cookies a enviarse. Es descendiente de TCollection, respectivamente el TCookie contenido es descendiente de TCollectionItem. Por lo tanto, puede usar TCollection en la forma de manejar items para manipularlo.

Aquí un ejemplo:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
var
  C: TCookie;
begin
  C := AResponse.Cookies.Add;
  C.Name := 'miCookie';
  C.Value := 'algunValor';
  Handled := true;
end;

No verá ninguna salida en su navegador. Pero si usa alguna herramienta de navegador (Chrome y Brave tienen una incorporada), puede ver el encabezado de la respuesta:

Encabezado de respuesta puesta de una Cookie response en las herramientas de desarrollador de Chrome's

Note que la cookie tiene atributos, así que Name y Value no son las únicas cosas que pueda poner. Navegue por la interfaz de TCookie para ver qué propiedades soporta.

Obteniéndola

Una vez se da el encabezado Set-Cookie arriba, la subsecuente petición a su sitio contendrá un encabezado adicional que contiene el valor que solicitó previamente poner:


Encabezado de petición de una Cookie en las herramientas de desarrollo de Chrome's

Afortunadamente, la forma de leerla no es distinta de los datos GET o POST. La propiedad relacionada es ARequest.CookieFields. Para leer una cookie previamente puesta:

procedure TFPWebModule1.DataModuleRequest(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
begin
  AResponse.Contents.Add('<p>La Cookie tiene:%d</p>', [ARequest.CookieFields.Values['miCookie']);
  Handled := true;
end;

Sesiones

TFPWebModule es un descendiente de TSessionHTTPModule, así que tiene capacidad de manejo de sesiones. La sesión está basada en módulos, así que cada módulo puede escoger usar o no el manejo de sesiones.

La sesión se implementa de forma abstracta. Por defecto, no se provee implementación. Un ejemplo de implementación usando archivos .ini está en la unidad iniwebsession. Debe tener esta unidad en su proyecto o implementar una para que el manejo de sesión funcione. Si decide implementar uno, básicamente necesita extender e implementar métodos abstractos en las clases TCustomSession y TSessionFactory.

Activando la Sesión

Para activar el manejo de sesión, ponga la propiedad CreateSession en true. La sesión será iniciada antes del manejo de la petición. En caso de una nueva sesión, OnNewSession será invocado. Inicialice sus variables de sesión aquí.

Manipulación de las Variables de Sesión

Las variables de sesión se proveen como Session.Variables (nb: el objeto Session es el equivalente al arreglo $_SESSION que se usa en PHP). Esto es una estructura parecida a mapa de cadenas, así que puede leer / escribir así:

Session.Variables['miVar'] := miVar; // escritura
...
miVar := Session.Variables['miVar']; // lectura

Poner una variable como cadena vacía NO la remueve. En vez de eso, si realmente quiere remover una variable, llame Session.RemoveVariable.

Terminando la Sesión

Llame Session.Terminate cuando quiera terminar una sesión (p.e.: logout del usuario). La sesión también expirará automáticamente si la siguiente petición viene después de Session.TimeOutMinutes desde la última petición. Cuando termina la sesión, OnSessionExpired será invocado. Haga toda la limpieza que necesite aquí.

Enrutamiento

Desde la versión FPC 3.0.4, un nuevo mecanismo se ha implementado. En vez de mantener compatibilidad con lo anterior, se ha decidido que el nuevo enrolamiento será por defecto. Así cualquier código anterior (o nuevo código dependiendo en el enrolamiento anterior) debe actualizarse agregando:

Application.LegacyRouting := true;

en el archivo .lpr.

Mecanismo Anterior

usando Múltiples Módulos

Puede tener múltiples módulos en su aplicación. Clique el menú "Archivo", luego clique "Nuevo…". Un cuadro de diálogo deberá aparecer, seleccione "Web Module".

Add new web module

luego clique en Aceptar.


Dado que existen múltiples módulos en su aplicación, ya no puede solicitar sólo con /. El marco de trabajo no podrá seleccionar mágicamente qué módulo debe servir la respuesta, por lo que hay dos formas de indicar a qué módulo le gustaría llamar:

  • /<nombre del módulo>
  • /?modulo=<nombre del módulo>

En el segundo formato, puede cambiar "módulo" (que es el valor por defecto) a cualquier clave de cadena de consulta válida modificando Application.ModuleVariable


Usando Actions

Hasta ahora, sólo hemos usado módulos web con un solo manejador de peticiones. Esto no escala mucho a medida que su aplicación web se vuelve más y más compleja. Además, algunas funciones pueden tener propiedades compartidas y es mejor agruparlas de forma lógica, por ejemplo:


  • Módulo Contable
    • Action para Ingreso
    • Action para Salir
    • Action para Registro
  • Módulo de Productos
    • Action para Crear
    • Action para Actualizar
    • Action para Eliminar
    • Action para Detalles


Flujo de Manejo de Peticiones

Antes de usar un Action, es importante que conozca el manejo de peticiones de fpWeb. Si no lo hace podría inutilizar su Action porque es siempre su módulo de datos que maneja la petición. Cómo podría pasar eso? Retroceda algunos capítulos, recuerda el Handled := true que siempre hicimos al final de cada procedimiento? Aquí es donde el parámetro Handled entra en juego.

Cada petición irá a través del OnRequest del módulo primero, sin importar la acción solicitada. Sólo si no se pone Handled en true, la acción web OnRequest se ejecuta.

En general, el flujo de peticiones es:

Flujo de peticiones fpWeb

Note el recuadro "Our Concern", es eso a lo que le pondremos nuestra atención.


Agregue Actions a los Módulos Web

Para agregar un Action, seleccione el módulo web luego vaya al Inspector de Objetos. En la pestaña de propiedades, seleccione Actions y clique el botón en la segunda columna.

Botón de manejo de acciones en el Inspector de Objetos

Una ventana popup deberá aparecer donde puede agregar, eliminar y cambiar el orden de las acciones.

Botón de manejo de acciones en la ventana popup

Presione Agregar, una nueva acción aparecerá en la lista. Selecciónela y luego vaya al Inspector de Objetos. Mostrará propiedades y eventos para esa nueva acción. Renombre la propiedad Name (este será el nombre que escribirá en la URL, así que dele un nombre corto, simple, pero descriptivo) como prefiera, escogeremos "Hola". Muévase a la pestaña de eventos, haga lo mismo que OnRequest para el módulo, clique el botón a la derecha de la fila OnRequest para crear el manejador de la petición.

Creando un manejador para la acción web OnRequest en el Inspector de Objetos

Se le presentará en la misma interfaz OnRquest, pero esto es una acción de manejo web en vez de un módulo web. Cualquier cosa que haga en el módulo web OnRequest puede hacerse aquí también. Copie el cuerpo del método desde la sección "Hola, Mundo!".

Recuerde remover Handled := true del cuerpo previo del módulo web OnRequest (o remueva el evento completamente) para que la acción esté pendiente del manejo de la petición.

Ejecute su proyecto, y encienda su navegador. Ahora, mientras se delega al manejo de petición la acción web, no podrá sólo pedir /, sino que necesita /<nombre de la acción> o propiedad ActionVar del Módulo>=<nombre de la acción>. Note que <propiedad del Módulo de ActionVar> tiene un valor por defecto de cadena vacía, a diferencia de Application.ModuleVariable el cual tiene "module" como el valor por defecto. Así que, por defecto, puede sólo usarse el formulario /<nombre de la acción>.

Si tiene múltiples módulos, entonces tiene una variedad de opciones:

  • /<nombre del módulo>/<nombre de la acción>
  • /<nombre del módulo>?action=<nombre de la acción>
  • /<nombre de la acción>?module=<nombre del módulo>
  • /?module=<nombre del módulo>&action=<nombre de la acción>

Note que tan pronto como un módulo tenga al menos una acción, /<nombre de la acción o del módulo> en solitario se dirigirá por defecto a /<nombre de la acción>. Para cambiar el comportamiento de tal forma que se dirija a /<nombre del módulo> por defecto, ponga Application.PreferModuleName en true. En el caso de múltiples módulos, si no se ha dado nombre de módulo, entonces el módulo por defecto manejará la acción dada. Para cambiar el comportamiento de ese módulo, el nombre debe darse de forma explícita, ponga Application.AllowDefaultModule en false.

Las siguientes tablas resumen lo que pasará basado en las dos propiedades:

/<nombre de la acción o del módulo> Application.PreferModuleName
true false
Application.AllowDefaultModule true /<nombre del módulo> /<módulo por defecto>/<nombre de la acción>
false /<nombre del módulo> ERROR
Acción por Defecto

Recuerde el diagrama previo, el "Delegar el manejo de peticiones a las acciones" actualmente no es tan simple, pero si expandimos el diagrama, la imagen será demasiado grande para que quepa. Así que aquí está el diagrama de esa parte:

Delegación de la petición al flujo de la acción

Dos cosas importantes del flujo: DefActionWhenUnknown y una acción por defecto. El formador es una propiedad del módulo web mientras que el último corresponde a la propiedad Default de una acción. En el último caso, en el que haya más de dos acciones que tengan la propiedad Default en true, el orden de las acciones (tal como se muestra en la ventana popup de manejo de acciones) será considerada a decidir cuál es la acción por defecto. Las dos propiedades forman lo que la aplicación debería hacer si no se encuentra una acción correspondiente para una petición dada.

Las siguientes tablas resumen lo que pasará basado en las dos propiedades:

Petición con un nombre de acción no-válido DefActionWhenUnknown
true false
Action.Default true Petición manejada por una acción por defecto Error: No se encontró acción para la acción: <nombre de la acción>
false Error: Nombre de acción no-válido y/o no acción por defecto Error: No se encontró acción para la acción: <nombre de la acción>


Petición sin nombre de acción, p.e.: / DefActionWhenUnknown
true false
Action.Default true Petición manejada por la acción por defecto Petición manejada por la acción por defecto
false Error: Sin Nombre de acción y sin acción por defecto Error: Sin Nombre de acción y sin acción por defecto

En caso de la respuesta de error arriba, una traza de pila deberá aparecer, completa con la información del número de línea si construyó su aplicación con la opción -gl. Veremos luego cómo crear un manejador personalizado para esta (las trazas de pila no son buenas para la producción). Pero por ahora, asegúrese que entiende el concepto del módulo de web y las acciones web, especialmente el flujo de peticiones. Juegue con el código hasta que crea estar listo para la siguiente sección.

Nuevo Mecanismo

El nuevo mecanismo es extremadamente flexible y funciona incluso sin módulos de datos (el viejo mecanismo sólo funciona con módulos de datos).

Una unidad dedicada para este enrolamiento se provee como httproute (agregue esto a la cláusula uses del programa o unidad donde quiera registrar las rutas). La unidad contiene una función HTTPRouter que retornará un objeto solitario responsable por la administración de las rutas de la aplicación y tiene un método RegisterRoute para registrar su ruta.

Sintáxis de la Ruta

El primer parámetro de HTTPRouter.RegisterRoute es la ruta que se emparejará con la petición entrante. Puede ser tan simple como un asterisco *, el cual significa 0 ó más rutas o simplemente cualquier ruta, tan complejo como /api/v1/:resource/* el cual significa REQUEST_URI el encabezado debería iniciar con /api/v1/ seguido por algo que será unido a la variable del recurso nombrada y finalmente termina con 0 o más rutas. Será ajustable con:

  • /api/v1/products
  • /api/v1/products/1
  • /api/v1/products/1/clone
  • /api/v1/products/something/else/that/is/really/long/and/silly

pero no con:

  • /api/v1
  • /excuse/me/api/v1/products

Básicamente hay sólo 3 caracteres especiales:

  • *  que denota 0 o más rutas
  • :param  denota una parte
  • /  separador de partes

su ruta se compondrá de estos 3 caracteres, además cualquier cosa que conforme una ruta.


Registrar una Ruta

El 2º, 3º o 4º parámetro (dependiendo en lo que desee, manejar un método HTTP específico y/o pasar datos adicionales en él) de HTTPRouter.RegisterRoute está sobrecargado con varias posibilidades:

  • Procedimiento Callback
TRouteCallback = Procedure(ARequest: TRequest; AResponse);
  • Evento Callback
TRouteEvent = Procedure(ARequest: TRequest; AResponse) of object;
  • Objeto que satisface una interfaz (CORBA)
IRouteInterface = Interface ['{10115353-10BA-4B00-FDA5-80B69AC4CAD0}']
  Procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);
end;
  • Objeto que extiende una clase de enrutador abstracto
TRouteObject = Class(TObject, IRouteInterface)
Public
  Procedure HandleRequest(ARequest: TRequest; AResponse: TResponse); virtual; abstract;
end;

TRouteObjectClass = Class of TRouteObject;

Por defecto, si el 2º parámetro no es TRouteMethod, entonces todos los métodos HTTP concordarán. Use alguno de estos rmUnknown, rmAll, rmGet, rmPost, rmPut, rmDelete, rmOptions, rmHead, rmTrace para concordar sólo con un método HTTP específico.

El orden de registro importa. Si hay dos o más rutas que concuerdan con la petición actual, el que se registró antes se manejará.

En este nuevo mecanismo, un simple programa Hola, Mundo! puede ser tan simple como:

uses
  fphttpapp, httpdefs, httproute;
procedure DigaHola(ARequest:TRequest; AResponse : TResponse);
begin
  AResponse.Content:='<html><body><h1>Hola, Mundo!</h1></body></html>';
end;

begin
  HTTPRouter.RegisterRoute('*', @DigaHola);
  Application.Port := 9000;
  Application.Initialize;
  Application.Run;
end.

Ejemplo Webserver

Este es un ejemplo de un servidor simple, multi-plataforma, multi-hilos.

program webserver;
 
{$mode objfpc}{$H+}
 
uses
  {$ifdef UNIX}
    cthreads, cmem,
  {$endif} 
  fphttpapp, httpdefs, httproute;
 
procedure route1(aReq: TRequest; aResp: TResponse);
begin
  aResp.content:='<html><body><h1>Ruta 1 Por Defecto</h1></body></html>'
end;
 
procedure route2(aReq: TRequest; aResp: TResponse);
begin
  aResp.content:='<html><body><h1>Ruta 2</h1></body></html>'
end;
 
begin
  HTTPRouter.registerRoute('/', @route1, true);
  HTTPRouter.registerRoute('/route2', @route2);
  Application.port := 8080;
  Application.threaded := true;
  Application.initialize;
  Application.run;
end.

Para abrir la "Ruta 1" teclee en su navegador la URL: http://localhost:8080

Para abrir la "Ruta 2" teclee en su navegador la URL: http://localhost:8080/route2

Puede cambiar localhost por 127.0.0.1

Usando Plantillas

fpWeb ha integrado soporte para FPTemplate, el motor de plantillas genérico de Free Pascal. No tiene que ser usado desde el contexto de la aplicación web, pero con soporte integrado las cosas serán un poco más fáciles. Al menos el administrador de memoria puede ser ignorado mientras el módulo se encarga de eso.

Hay dos niveles donde se puede usar plantillas: action y módulo. El soporte completo del RAD está incompleto, así que necesitará codificar por su cuenta en algunos puntos.

Hay dos modos de operación: parametrizados y no parametrizados. El modo activo está controlado por la propiedad AllowTagParams, la cual deberá ser suficientemente obvia con los valores que se refieren a los modos.

La cadena de plantilla puede obtenerse desde un archivo a través de la propiedad FileName o una cadena directa a través de la propiedad de plantilla Template. Si ambas propiedades contienen valores, entonces tendrá preferencia FileName.

Las dos propiedades: StartDelimiter y EndDelimiter definen cómo el motor debería reconocer una etiqueta de plantilla. Por ejemplo, si se tiene:

  • StartDelimiter = '{+'
  • EndDelimiter = '+}'

entonces una cadena '{+titulo+}' define una plantilla llamada 'titulo'. Nótese que los espacios son importantes, así que '{+ titulo +}' define una plantilla llamada ' titulo ' en vez de sólo 'titulo'.

En especial para el modo parametrizado, tres propiedades adicionales: ParamStartDelimiter, ParamEndDelimiter y ParamValueSeparator definen cómo el motor debe reconocer un parámetro de etiqueta de plantilla. Por ejemplo, si tiene:

  • ParamStartDelimiter = '[-'
  • ParamEndDelimiter = '-]'
  • ParamValueSeparator = '='

entonces una cadena '{+data [-p=v-][-a=b-] +}' define una etiqueta de plantilla llamada 'data' con el parámetro 'p' de valor 'v' y el parámetro 'a' de valor 'b'. Puede usarse para pasar el parámetro de nivel de plantilla tal como se espera, en formato de fecha, encabezado-fila-pieDePágina para una presentación personalizada, nombre de archivo, etc. usted decide.

Como consecuencia de distintas formas de operar, el evento central donde la etiqueta de plantilla trabaja es diferente también.

El no-parametrizado usará OnGetParam mientras que el parametrizado usará OnReplaceTag. Los dos tienen por supuesto, diferentes interfaces:

Type
  // OnGetParam: para simple soporte exclusivo de etiquetas de plantilla (ej: {Nombre})
  TGetParamEvent = Procedure(
    Sender: TObject;
    Const ParamName: String;
    Out AValue: String
  ) Of Object;
  // OnReplaceTag: para etiquetas con soporte de parámetros
  TReplaceTagEvent = Procedure(
    Sender: TObject;
    Const TagString: String;
    TagParams: TStringList;
    Out ReplaceText: String
  ) Of Object;

En OnGetParam, compruebe ParamName, luego asigne AValue respectivamente. p.e., si quiere etiquetar 'titulo' para ser reemplazado por 'Mi App', entonces llene el método con:

// use Trim() si quiere que los espacios alrededor de la etiqueta no importen
case Trim(ParamName) of
  'titulo': AValue := 'Mi App';
else
  AValue := 'DESCONOCIDO';
end;

En OnReplaceTag, compruebe TagString y opcionalmente TagParams, luego asigne ReplaceText respectivamente. p.e., si quiere que la etiqueta 'FechaHora' se reemplace con el tiempo actual con el parámetro 'datetimeformat' para especificar cómo la fecha y hora deberían formatearse, luego llene en el método así:

// use Trim() si quiere que los espacios alrededor de la etiqueta no importen
case Trim(TagString) of
  'FechaHora': AValue := FormatDateTime(TagParams.Values['datetimeformat'],Now);
else
  AValue := 'DESCONOCIDO';
end;

Al Nivel de la Acción

Cree/seleccione una acción, luego vaya al inspector de objetos. Verá una propiedad de subcomponente llamada Template. Esta plantilla es una instancia normal de TFPTemplate. Expándalo y llene en las propiedades como se explicó arriba. Ahora vaya a la pestaña Eventos, nuevamente expanda Template, verá los dos eventos. Llene el que está basado en su valor de propiedad de AllowTagParams.

NOTA: Si su Lazarus no puede autocompletar el evento, intente escribir el nombre manualmente en el cuadro de edición y luego clique el botón ···. Este es un bug en el Lazarus actual que deberá ser arreglado en el futuro.

A este nivel, la plantilla con contenido no se ajusta automáticamente como el manejador de petición. Podría cambiar en el futuro, pero trabajemos con su estado actual. Llene el evento de la acción OnRequest con:

with Actions.CurrentAction as TFPWebAction do
begin
  AResponse.Content := Template.GetContent;
end;
Handled := true;

El cast es requerido desde que CurrentAction es de tipo TCustomWebAction en vez de TFPWebAction. Sin eso, no podemos acceder a la propiedad Template.

A Nivel de Módulo

A nivel del módulo, actualmente tiene que codificarlo porque no se ha implementado soporte para RAD. La propiedad a vincular es ModuleTemplate. Esto es sin embargo una instancia irregular de TFPTemplate, pero una clase especial TFPWebTemplate que es descendiente de ella.

La idea aquí es que el módulo proporcione un diseño mientras que las acciones proporcionan contenido, con la capacidad de proporcionar variables adicionales. Por tanto, es buena idea mantener AllowTagParams como está y asignar el evento OnGetParam **del módulo** solamente. NO asigne OnGetParam del ModuleTemplate ya que nunca será llamado.

Una etiqueta de plantilla llamada 'contenido' se reemplazará automáticamente por el contendido producido por la acción, todo lo demás es reemplazado desde las variables internas de plantilla o desde OnGetParam.

Usando Plantillas por Separado

A pesar de la integración algo incompleta, nada le impide usar fpTemplate (o cualquier otra solución de plantillas) manualmente, fuera del soporte integrado. Esto podría incluso ser mejor en algunos casos ya que es modular.


Usando un objeto html Producer

Suponiendo que hay una TWebAction llamada act_get_my_html, debe solicitar escribir en AResponse, el retorno de una corriente de bytes interna: al responder con la solución de objetos html Producer, debemos usar los métodos del escritor. Maneja una corriente de memoria con el propósito de mayor rapidez. No hay tipo de cadena aquí, como con la solución fpTemplate anterior que utiliza el procesamiento de texto \ cadena como en la forma normal de PHP. El Productor escribe en AResponse con una iteración recursiva " para cada elemento ", que atraviesa el árbol HTML DOM de objetos polimórficos, compuesto por la jerarquía de clases THtmlCustomElement = Class(TDOMElement) (vea la unidad htmlelements.pp). Entonces, AResponse se escribe mediante el parámetro aWriter, con la siguiente llamada:

procedure TFPWebModule1.act_get_my_htmlRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse; var Handled: Boolean);
begin
  (* pide a HTMLEntityProducer1 el contenido de sus elementos html DOM: *)
  HTMLEntityProducer1.HandleRequest(ARequest, AResponse, Handled);
  Handled := True;
end;

Dicho de nuevo, es trabajo del Productor convertir su corriente interna de memoria a texto en el objeto de la respuesta: así es como se diseñó este patrón, aunque hay un método de representación de texto-html llamado ProduceContent solo con fines de depuración. Puede sobrecargar este método si está escribiendo o depurando un componente Producer.

Aquí hay un ejemplo, con un objeto Producer html, que permite crear una aplicación web al "modo RAD", es decir, con el arrastrar y soltar del componente HTMLEntityProducer desde la paleta de componentes:

procedure TFPWebModule1.HTMLEntityProducer1WriteEntity(Sender: THTMLContentProducer; aWriter: THTMLWriter);
begin  // HTMLEntityProducer has Entity := heHtml
  aWriter.StartHead; // .startHeader;
    aWriter.Meta('','Content-Type','text/html; charset=UTF-8');
    aWriter.title('My web page');
    aWriter.link('stylesheet','stylesheet.css','text/css','screen');
  aWriter.EndHead; // .endHeader;

  aWriter.Startbody;
    aWriter.StartParagraph;  
      aWriter.heading2('Hola, mundo desde adentro §1:');
      aWriter.Text('Esto es texto escrito dentro del párrafo actual.');
    aWriter.EndParagraph; 
    aWriter.paragraph('Otro texto dentro de un párrafo "iniciado y terminado" por sí mismo.');
    aWriter.StartP; // .StartParagraph;
      aWriter.tagH2('Hola, mundo desde adentro §2:'); // .heading2;
      aWriter.Text('Aquí está el texto final.');
      aWriter.Image.src := 'logo.png';
      AnotherProducer.WriteContent(aWriter);
    aWriter.EndP; // .EndParagraph;
    // you can use tags and other THtmlCustomElements
    aWriter.tagP([aWriter.bold('Hola'), aWriter.tagQ('Mundo!')]);
    aWriter.Text('<u>Hola,</u><i>mundo!</i>');
  aWriter.Endbody;

end;

La propiedad TWebAction.ContentProducer permite acoplar un THTTPContentProducer a su acción web expuesta a través de una URI en la red.

[PorHacer: no hay documentación oficial acerca de los componentes "RAD way" de ftweb (html producer, html provider, html adapter, html formatter, etc)]

Trucos y Consejos

Retornando Distintos Códigos de Respuesta HTTP

Por defecto, fpWeb retornará HTTP 200 OK para indicar un manejo de respuesta exitoso. Esto de seguro no siempre es el caso, como las entradas del usuario no puedan ser lo que esperamos. Para hacerlo, modifique AResponse.Code en su manejador de peticiones en el código que desee retornar.

Redirigir Peticiones a Diferentes URLs

Un flujo común después de un login es redirigir al usuario a su página de cuenta. Esto se puede hacer llamando AResponse.SendRedirect en su manejador de petición, ordenando a la URL que redirija la petición.

Sirviendo Archivos Estáticos (Servidor Web Embebido)

Recuerda el diálogo en la sección #Hola, Mundo! después que seleccione la Aplicación servidora HTTP? Si escogió "Registre ubicación de dónde servir archivos" puede llenar "Ubicación" (el segmento URI, no debe contener ningún slash) y "Directorio" (directorio físico en su computador, debe existir en tiempo de ejecución) y el asistente simplemente agrega:

RegisterFileLocation('<Location>','<Directory>');

al comienzo de su archivo .lpr y agrega la unidad fpwebfile a su cláusula uses. Puede actualmente hacer esto a mano en cualquier momento y también registrar múltiples veces para diferentes ubicaciones / directorios. Después de esto puede pedir /<Ubicación>/<cualquier nombre de archivo dentro del Directorio> y será servido automáticamente. Note que el mimetype del archivo se determina por fpmimetypes. Llame MimeTypes.LoadFromFile con su archivo mime.types para dar un mimetype correcto basado en su extensión. De otra forma, el archivo siempre se servirá como application/octet-stream el cual significa que el navegador lo descargará en vez de interpretarlo (especialmente importante para JavaScript y archivos CSS).

Puede aprovechar los mime.types completos aquí http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types?&view=co

En Lazarus 2.0.6 o más reciente versión debe agregar en el comienzo del programa la ruta completa de los archivos mime.type

begin
  MimeTypesFile := Application.Location + 'mime.txt';

Tenga en cuenta que la ruta por defecto que viene con el proyecto es lib\$(TargetCPU)-$(TargetOS)

por ejemplo httpproject\lib\i386-win32\mime.txt

Centralice la Administración de la Configuración y los Módulos

Por defecto, el archivo (.lpr) es el único que contiene la unidad de protocolo. Esto limita la habilidad de usar el objeto Application de otros contextos como los módulos web. Afortunadamente, esto no dificulta refactorizar lo que queremos tener. Removemos las llamadas RegisterHTTPModule desde las unidades de los módulos web dejamos al .lpr que extraiga para vaciar el bloque principal con un identificador simple de unidad en la cláusula uses, lo nombramos: agentes (Brokers). La unidad contiene:

unit Brokers;

{$mode objfpc}{$H+}

interface

{ $define cgi}
{ $define fcgi}
{$define httpapp}

uses
  CustWeb;

function GetApp: TCustomWebApplication; inline;

implementation

uses
  {$ifdef cgi}fpcgi{$endif}
  {$ifdef fcgi}fpfcgi{$endif}
  {$ifdef httpapp}fphttpapp{$endif}
  ,webmodule1
  ,webmodule2
  ;

function GetApp: TCustomWebApplication;
begin
  Result := Application;
end;

initialization
  RegisterHTTPModule('wm1', TWebModule1);
  RegisterHTTPModule('wm2', TWebModule2);
  {$ifndef cgi}
  Application.Port := 2015;
  {$endif}
  Application.Initialize;
  Application.Run;
end.

De esta forma, podemos controlar el registro del módulo web y también proveer un API para obtener el objeto Application (enmarcado como TCustomWebApplication), mientras todavía de forma fácil cambie entre los protocolos de implementación, en un sólo lugar.


Terminando Adecuadamente (FastCGI / Servidor Web Embebido)

En vez de terminar la aplicación mediante Ctrl+C, hay una forma para que su aplicación termine adecuadamente, haciendo todo lo necesario para limpiar lo que necesite, llamando Application.Terminate. Podría necesitar usar el truco previo para acceder fácilmente al objeto Application. Una implementación común es proveer un módulo / acción específico protegido con contraseña que llame el método Terminate. Puede escoger la forma que prefiera.

Manejador de Excepción Personalizada

[edición de Mayo 1 de 2020 => movido desde un puntero de método a un simple de procedimiento.]

Para pasar por delante del manejador de excepción por defecto, el cual imprime stacktrace cada vez que una excepción se genera (p.e.: HTTP 404 ó 500), y así no es bueno para la producción, debe asignar un Application.OnShowRequestException.

Este es un método que necesitará proveer su procedimiento que implemente el método y lo asigne usando el objeto. p.e.: si tiene MyExceptionHandler como un objeto TMyExceptionHandler el cual tiene un método MyShowRequestException, puede asignarlo mediante:

Application.OnShowRequestException := @MyExceptionHandler.MyShowRequestException;

no olvide .Create() MyExceptionHandler ANTES de asignar arriba u obtendrá un EAccessViolation!

Debe proveer un procedimiento global que implemente su propio manejador de excepción (en producción, es You must provide your global procedure that implements your own exception handler (in production, es recomendable reemplazar la pila de llamadas por un código de estado HTTP y su explicación). Luego puede pasar por delante el manejador de excepción por defecto, asignándolo así:

Application.OnShowRequestException := @MyShowRequestException;

Codificando Sólo a Mano (Sin Diseñador de Formularios)

No es una obligación usar el diseñador de formularios para escribir una aplicación fpWeb. Puede usar técnicas de codificación a mano para escribirla. El secreto radica en el 3er parámetro de RegisterHTTPModule : SkipStreaming. Cuando este parámetro se pone en verdadero, fpWeb no buscará el recurso .lfm. Por tanto todo debe manejarse de forma manual: las opciones de propiedades, los maneadores de eventos, registro de acciones, etc.

Note que es lógico hacer lo que usualmente funciona a través del inspector de objetos en un constructor solapado. Dentro de él, llame al constructor heredado supliendo los parámetros AOwner y CreateMode. Después de eso puede poner propiedades, asignar maneadores de eventos, etc. Por ejemplo:

type
  TModuloHolaMundo = class(TFPWebModule)
    constructor CreateNew(AOwner: TComponent; CreateMode: Integer); override;
    procedure Request(Sender: TObject; ARequest: TRequest;
      AResponse: TResponse; var Handled: Boolean);
  end;

constructor TModuloHolaMundo.CreateNew(AOwner: TComponent; CreateMode: Integer);
begin
  inherited CreateNew(AOwner, CreateMode);
  OnRequest := @Request;
end;

procedure TModuloHolaMundo.Request(Sender: TObject; ARequest: TRequest;
 AResponse: TResponse; var Handled: Boolean);
begin
  AResponse.Content := 'Hola, Mundo!';
  Handled := true;
end;


Vea También

Screenshot copia de uno de los ejemplos

  • XML o JSON, en ExtJS: si quiere usar un motor más o menos complejo que sea bastante "cumplidor" sin importar el aspecto que muestran los objetos Javascript en el lado del navegador, además de sus funcionalidades (como ExtJS, por ejemplo, para mostrar una grilla accesible a la base de datos, gráficos dinámicos, etc), esta clase de solución muchas veces espera por un archivo XML o JSON. Hay clases adaptadoras y formateadoras (p.e. las clases TExtJSXMLWebdataInputAdaptor, TExtJSJSONDataFormatter).

Sobre todo, las clases adaptadoras "adaptan" los nodos de un JSON o XML entrante a un mapeo de sus campos de base de datos TField. Y las clases formateadoras son mapeadores de cada Field de un registro en su nodo en formato JSON o XML, antes de enviarlo hacia el navegador.

traducido por edgarrod71@gmail.com