Citizen es un marco de aplicaciones web basado en MVC diseñado para personas interesadas en construir rápidamente sitios web rápidos y escalables en lugar de cavar alrededor de las agallas del nodo o juntar una torre Jenga tambaleante hecha de 50 paquetes diferentes.
Use Citizen como base para una aplicación web tradicional del lado del servidor, una aplicación modular de una sola página (SPA) o una API RESTFUL.
Hubo numerosos cambios de ruptura en la transición de 0.9.x a 1.0.x. Consulte a ChangeLog para una lista detallada y revise esta documentación actualizada a fondo.
Claramente, este es mucho más contenido del que debe contener cualquier NPM/GitHub ReadMe. Estoy trabajando en un sitio para esta documentación.
Utilizo Citizen en mi sitio personal y originalTrilogy.com. OT.com maneja una cantidad moderada de tráfico (unos cientos de miles de vistas cada mes) en un plan de alojamiento de la nube de $ 30 que ejecuta una sola instancia de Citizen, donde la aplicación/proceso se ejecuta durante meses a la vez sin bloquear. Es muy estable.
Estos comandos crearán un nuevo directorio para su aplicación web, instalarán ciudadanos, utilizarán su utilidad de andamio para crear el esqueleto de la aplicación e iniciar el servidor web:
$ mkdir myapp && cd myapp
$ npm install citizen
$ node node_modules/citizen/util/scaffold skeleton
$ node app/start.jsSi todo salió bien, verá la confirmación en la consola que el servidor web está ejecutando. Vaya a http://127.0.0.1:3000 en su navegador y verá una plantilla de índice desnudo.
Citizen utiliza literales de plantilla en su motor de plantilla predeterminado. Puede instalar Consolidate, actualizar la configuración de la plantilla y modificar las plantillas de vista predeterminadas en consecuencia.
Para las opciones de configuración, consulte la configuración. Para obtener más servicios públicos para ayudarlo a comenzar, vea los servicios públicos.
app/
config/ // These files are all optional
citizen.json // Default config file
local.json // Examples of environment configs
qa.json
prod.json
controllers/
hooks/ // Application event hooks (optional)
application.js
request.js
response.js
session.js
routes/ // Public route controllers
index.js
helpers/ // Utility modules (optional)
models/ // Models (optional)
index.js
views/
error/ // Default error views
404.html
500.html
ENOENT.html
error.html
index.html // Default index view
start.js
logs/ // Log files
access.log
error.log
web/ // public static assets
Importar ciudadano y comenzar su aplicación:
// start.js
import citizen from 'citizen'
global . app = citizen
app . start ( )Correr desde la terminal:
$ node start.jsPuede configurar su aplicación ciudadana con un archivo de configuración, opciones de inicio y/o configuraciones de controlador personalizadas.
El directorio de configuración es opcional y contiene archivos de configuración en formato JSON que impulsan tanto el ciudadano como su aplicación. Puede tener múltiples archivos de configuración de ciudadanos dentro de este directorio, lo que permite diferentes configuraciones basadas en el entorno. Citizen construye su configuración en función de la siguiente jerarquía:
host que coincida con el nombre de host de la máquina, y si encuentra uno, extiende la configuración predeterminada con la configuración del archivo.host coincidente, busca un archivo llamado Citizen.json y carga esa configuración.Supongamos que desea ejecutar Citizen en el puerto 8080 en su entorno de desarrollo local y tiene una base de datos local a la que su aplicación se conectará. Puede crear un archivo de configuración llamado local.json (o dev.json, lo que quiera) con lo siguiente:
{
"host" : "My-MacBook-Pro.local" ,
"citizen" : {
"mode" : "development" ,
"http" : {
"port" : 8080
}
} ,
"db" : {
"server" : "localhost" , // app.config.db.server
"username" : "dbuser" , // app.config.db.username
"password" : "dbpassword" // app.config.db.password
}
}Esta configuración ampliaría la configuración predeterminada solo cuando se ejecuta en su máquina local. Usando este método, puede confirmar múltiples archivos de configuración de diferentes entornos al mismo repositorio.
La configuración de la base de datos estaría accesible en cualquier lugar dentro de su aplicación a través de app.config.db . Los nodos citizen y host están reservados para el marco; Cree sus propios nodos para almacenar su configuración personalizada.
Puede configurar la configuración de su aplicación al inicio a través de app.start() . Si hay un archivo de configuración, la configuración de inicio ampliará el archivo de configuración. Si no hay archivo de configuración, la configuración de inicio extiende la configuración ciudadana predeterminada.
// Start an HTTPS server with a PFX file
app . start ( {
citizen : {
http : {
enabled : false
} ,
https : {
enabled : true ,
pfx : '/absolute/path/to/site.pfx'
}
}
} ) Para establecer configuraciones personalizadas a nivel de controlador de ruta, exporte un objeto config (más sobre controladores de ruta y acciones en la sección de controladores de ruta).
export const config = {
// The "controller" property sets a configuration for all actions in this controller
controller : {
contentTypes : [ 'application/json' ]
}
// The "submit" property is only for the submit() controller action
submit : {
form : {
maxPayloadSize : 1000000
}
}
} Lo siguiente representa la configuración predeterminada de Citizen, que se extiende por su configuración:
{
host : '' ,
citizen : {
mode : process . env . NODE_ENV || 'production' ,
global : 'app' ,
http : {
enabled : true ,
hostname : '127.0.0.1' ,
port : 80
} ,
https : {
enabled : false ,
hostname : '127.0.0.1' ,
port : 443 ,
secureCookies : true
} ,
connectionQueue : null ,
templateEngine : 'templateLiterals' ,
compression : {
enabled : false ,
force : false ,
mimeTypes : [
'application/javascript' ,
'application/x-javascript' ,
'application/xml' ,
'application/xml+rss' ,
'image/svg+xml' ,
'text/css' ,
'text/html' ,
'text/javascript' ,
'text/plain' ,
'text/xml'
]
} ,
sessions : {
enabled : false ,
lifespan : 20 // minutes
} ,
layout : {
controller : '' ,
view : ''
} ,
contentTypes : [
'text/html' ,
'text/plain' ,
'application/json' ,
'application/javascript'
] ,
forms : {
enabled : true ,
maxPayloadSize : 524288 // 0.5MB
} ,
cache : {
application : {
enabled : true ,
lifespan : 15 , // minutes
resetOnAccess : true ,
encoding : 'utf-8' ,
synchronous : false
} ,
static : {
enabled : false ,
lifespan : 15 , // minutes
resetOnAccess : true
} ,
invalidUrlParams : 'warn' ,
control : { }
} ,
errors : 'capture' ,
logs : {
access : false , // performance-intensive, opt-in only
error : {
client : true , // 400 errors
server : true // 500 errors
} ,
debug : false ,
maxFileSize : 10000 ,
watcher : {
interval : 60000
}
} ,
development : {
debug : {
scope : {
config : true ,
context : true ,
cookie : true ,
form : true ,
payload : true ,
route : true ,
session : true ,
url : true ,
} ,
depth : 4 ,
showHidden : false ,
view : false
} ,
watcher : {
custom : [ ] ,
killSession : false ,
ignored : / (^|[/\]).. / // Ignore dotfiles
}
} ,
urlPath : '/' ,
directories : {
app : < appDirectory > ,
controllers : < appDirectory > + '/controllers',
helpers : < appDirectory > + '/helpers',
models : < appDirectory > + '/models',
views : < appDirectory > + '/views',
logs : new URL('../../../logs', import.meta.url).pathname
web : new URL('../../../web', import.meta.url).pathname
}
}
} Aquí hay un resumen completo de la configuración de Citizen y lo que hacen.
Al iniciar un servidor, además de las opciones de configuración http y https de Citizen, puede proporcionar las mismas opciones que HTTP.CreateServer () y https.createserver () de Node.
La única diferencia es cómo pasa archivos clave. Como puede ver en los ejemplos anteriores, aprueba a Citizen las rutas de archivo para sus archivos clave. Citizen lee los archivos para usted.
| Configuración | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|
host | Cadena | '' | Para cargar diferentes archivos de configuración en diferentes entornos, Citizen se basa en el nombre de host del servidor como clave. Al inicio, si Citizen encuentra un archivo de configuración con una clave host que coincide con el nombre de host del servidor, elige ese archivo de configuración. Esto no debe confundirse con el hostname del servidor HTTP (ver más abajo). |
| ciudadano | |||
mode | Cadena | Verifica NODE_ENV primero, de lo contrario production | El modo de aplicación determina ciertos comportamientos de tiempo de ejecución. Los valores posibles son los registros de consola de silencios de producción de producción production y development . El modo de desarrollo permite registros de consolas verbosas, opciones de depuración de URL y reemplazo del módulo caliente. |
global | Cadena | app | La convención para inicializar a los ciudadanos en el archivo de inicio asigna el marco a una variable global. El valor predeterminado, que verá referenciado a lo largo de la documentación, es app . Puede cambiar esta configuración si desea usar otro nombre. |
contentTypes | Formación | [ 'text/html', 'text/plain', 'application/json', 'application/javascript' ] | Una lista de formatos de respuesta para cada solicitud, basada en el encabezado de solicitud Accept del cliente. Al configurar los formatos disponibles para controladores o acciones de ruta individuales, se debe proporcionar toda la variedad de formatos disponibles. |
errors | Cadena | capture | Cuando su aplicación arroja un error, el comportamiento predeterminado es que el ciudadano intente recuperarse del error y mantener la aplicación en funcionamiento. Establecer esta opción para exit le dice a Citizen que registre el error y salga del proceso. |
templateEngine | Cadena | templateLiterals | Citizen usa [plantilla literal] (https://developer.mozilla.org/en-us/docs/web/javascript/reference/template_literals) sintaxis para ver la representación de forma predeterminada. Opcionalmente, puede instalar Consolidate y usar cualquier motor que admita (por ejemplo, instalar manillar y establecer templateEngine en handlebars ). |
urlPath | Cadena | / | Denota la ruta de la URL que conduce a su aplicación. Si desea que su aplicación sea accesible a través de http://yoursite.com/my/app y no está utilizando otro servidor como un delantero para proxy de la solicitud, esta configuración debe ser /my/app (no olvide el corte principal). Esta configuración es necesaria para que el enrutador funcione. |
| http | |||
enabled | Booleano | true | Habilita el servidor HTTP. |
hostname | Cadena | 127.0.0.1 | El nombre de host en el que se puede acceder a su aplicación a través de HTTP. Puede especificar una cadena vacía para aceptar solicitudes en cualquier nombre de host. |
port | Número | 3000 | El número de puerto en el que el servidor HTTP del ciudadano escucha las solicitudes. |
| https | |||
enabled | Booleano | false | Habilita el servidor HTTPS. |
hostname | Cadena | 127.0.0.1 | El nombre de host en el que se puede acceder a su aplicación a través de HTTPS. El valor predeterminado es localhost, pero puede especificar una cadena vacía para aceptar solicitudes en cualquier nombre de host. |
port | Número | 443 | El número de puerto en el que el servidor HTTPS del ciudadano escucha las solicitudes. |
secureCookies | Booleano | true | Por defecto, todas las cookies establecidas dentro de una solicitud HTTPS son seguras. Establezca esta opción en false para anular ese comportamiento, haciendo que todas las cookies sean inseguras y requeriendo que establezca manualmente la opción secure en la directiva de cookies. |
connectionQueue | Entero | null | El número máximo de solicitudes entrantes para hacer cola. Si no se especifica, el sistema operativo determina el límite de cola. |
| sesiones | |||
enabled | Booleano | false | Habilita el alcance de la sesión del usuario, que le asigna a cada visitante una ID única y le permite almacenar datos asociados con esa ID dentro del servidor de aplicaciones. |
lifespan | Entero positivo | 20 | Si las sesiones están habilitadas, este número representa la longitud de la sesión de un usuario, en minutos. Las sesiones caducan automáticamente si un usuario ha estado inactivo durante este tiempo. |
| disposición | |||
controller | Cadena | '' | Si usa un controlador de diseño global, puede especificar el nombre de ese controlador aquí en lugar de usar la next directiva en todos sus controladores. |
view | Cadena | '' | De manera predeterminada, el controlador de diseño utilizará la vista de diseño predeterminada, pero puede especificar una vista diferente aquí. Use el nombre del archivo sin la extensión del archivo. |
| formularios | |||
enabled | Booleano | true | Citizen proporciona procesamiento básico de carga útil para formularios simples. Si prefiere usar un paquete de formulario separado, configure esto en false . |
maxPayloadSize | Entero positivo | 524288 | Tamaño de carga útil de forma máxima, en bytes. Establezca un tamaño de carga útil máxima para evitar que su servidor se sobrecargue mediante datos de entrada de formulario. |
| compresión | |||
enabled | Booleano | false | Habilita la compresión GZIP y deflate para vistas renderizadas y activos estáticos. |
force | Booleano o cuerda | false | Fuerza a GZIP o desinflen la codificación para todos los clientes, incluso si no informan aceptar formatos comprimidos. Muchos proxies y firewalls rompen el encabezado de codificación de aceptación que determina el soporte de GZIP, y dado que todos los clientes modernos admiten GZIP, generalmente es seguro forzarlo estableciendo esto en gzip , pero también puede forzar deflate . |
mimeTypes | Formación | Consulte la configuración predeterminada arriba. | Una variedad de tipos de MIME que se comprimirán si la compresión está habilitada. Consulte la configuración de muestra anterior para la lista predeterminada. Si desea agregar o eliminar elementos, debe reemplazar la matriz en su totalidad. |
| cache | |||
control | Objeto que contiene pares de clave/valor | {} | Use esta configuración para establecer encabezados de control de caché para controladores de ruta y activos estáticos. La clave es el nombre de ruta del activo, y el valor es el encabezado de control de caché. Consulte el almacenamiento en caché del lado del cliente para más detalles. |
invalidUrlParams | Cadena | warn | La opción de caché de ruta puede especificar parámetros de URL válidos para evitar que las URL malas se almacenen en caché, e invalidUrlParams determina si registrar una advertencia al encontrar URL malas o lanzar un error del lado del cliente. Consulte las solicitudes de almacenamiento en caché y las acciones del controlador para más detalles. |
| Aplicación de caché | |||
enabled | Booleano | true | Habilita el caché en memoria, accedido a través de los métodos cache.set() y cache.get() . |
lifespan | Número | 15 | El período de tiempo que un activo de aplicación en caché permanece en la memoria, en minutos. |
resetOnAccess | Booleano | true | Determina si reiniciar el temporizador de caché en un activo en caché cada vez que se accede al caché. Cuando se establecen en false , los elementos almacenados en caché caducan cuando se alcanza la lifespan . |
encoding | Cadena | utf-8 | Cuando pasa una ruta de archivo a cache.set (), la configuración de codificación determina qué codificación debe usarse al leer el archivo. |
synchronous | Booleano | false | Cuando pasa una ruta de archivo a cache.set (), esta configuración determina si el archivo debe leerse sincrónicamente o asíncronamente. Por defecto, las lecturas de archivos son asíncronas. |
| cache.static | |||
enabled | Booleano | false | Al servir archivos estáticos, Citizen normalmente lee el archivo desde el disco para cada solicitud. Puede acelerar el archivo estático que sirve considerablemente estableciendo esto en true , que almacena en caché los buffers de archivos en la memoria. |
lifespan | Número | 15 | El período de tiempo un activo estático en caché permanece en la memoria, en minutos. |
resetOnAccess | Booleano | true | Determina si restablece el temporizador de caché en un activo estático en caché cada vez que se accede al caché. Cuando se establecen en false , los elementos almacenados en caché caducan cuando se alcanza la lifespan . |
| registro | |||
access | Booleano | false | Habilita los archivos de registro de acceso HTTP. Desactivado de forma predeterminada porque los registros de acceso pueden explotar de manera rápida e ideal, debe ser manejado por un servidor web. |
debug | Booleano | false | Habilita los archivos de registro de depuración. Útil para depurar problemas de producción, pero extremadamente detallados (los mismos registros que vería en la consola en modo de desarrollo). |
maxFileSize | Número | 10000 | Determina el tamaño máximo de archivo de los archivos de registro, en Kilobytes. Cuando se alcanza el límite, el archivo de registro cambia de nombre con una marca de tiempo y se crea un nuevo archivo de registro. |
| Logs.Error | |||
client | Booleano | true | Habilita el registro de errores de cliente de 400 niveles. |
server | Booleano | false | Habilita el registro de errores de servidor/aplicación de 500 niveles. |
status | Booleano | false | Controla si los mensajes de estado deben registrarse en la consola cuando está en modo de producción. (El modo de desarrollo siempre registra la consola). |
| Logs.watcher | |||
interval | Número | 60000 | Para los sistemas operativos que no admiten eventos de archivos, este temporizador determina con qué frecuencia se encuestará los archivos de registro para los cambios antes del archivo, en milisegundos. |
| desarrollo | |||
| desarrollo.debug | |||
scope | Objeto | Esta configuración determina qué ámbitos se registran en la salida de depuración en modo de desarrollo. Por defecto, todos los ámbitos están habilitados. | |
depth | Entero positivo | 3 | Cuando Citizen arroja un objeto en el contenido de depuración, lo inspecciona usando el Inspecto de Node. Esta configuración determina la profundidad de la inspección, lo que significa el número de nodos que se inspeccionarán y mostrarán. Los números más grandes significan una inspección más profunda y un rendimiento más lento. |
view | Booleano | false | Establezca esto en True para volcar la información de depuración directamente en la vista HTML. |
enableCache | Booleano | false | El modo de desarrollo deshabilita el caché. Cambie esta configuración a true para habilitar el caché en modo de desarrollo. |
| Desarrollo. | |||
custom | Formación | Puede decirle al reemplazo del módulo Hot de Citizen que vea sus propios módulos personalizados. Esta matriz puede contener objetos con ruta watch (directorio relativo a sus módulos dentro del directorio de aplicaciones) y assign (la variable a la que asigna estos módulos) propiedades. Ejemplo:[ { "watch": "/util", "assign": "app.util" } ] | |
Citizen usa Chokidar como su observador de archivos, por lo que la opción watcher tanto para registros como para el modo de desarrollo también acepta cualquier opción permitida por Chokidar.
Estas configuraciones están expuestas públicamente a través de app.config.host y app.config.citizen .
Esta documentación supone que su nombre de variable global es app . Ajustar en consecuencia.
app.start() | Inicia un servidor de aplicaciones web ciudadano. |
app.config | La configuración de configuración que suministró al inicio. La configuración de Citizen está dentro de app.config.citizen . |
app.controllersapp.modelsapp.views | Es poco probable que necesite acceder a controladores y vistas directamente, pero hacer referencia app.models en lugar de importar sus modelos se beneficia manualmente del reemplazo de módulos caliente incorporado de Citizen. |
app.helpers | Todos los módulos de ayuda/utilidad colocados en app/helpers/ se importan al objeto ayudante. |
app.cache.set()app.cache.get()app.cache.exists()app.cache.clear() | Cache de aplicación y almacenamiento de clave/valor utilizado internamente por Citizen, también disponible para su aplicación. |
app.log() | La consola básica y el registro de archivos utilizados por Citizen, exportados para su uso. |
La estructura de la URL ciudadana determina qué controlador y acción enruta para disparar, pasa los parámetros de URL y deja un poco de espacio para el contenido amigable con SEO que puede duplicarse como un identificador único. La estructura se ve así:
http://www.site.com/controller/seo-content/action/myAction/param/val/param2/val2
Por ejemplo, digamos que la URL base de su sitio es:
http://www.cleverna.me
El controlador de ruta predeterminado es index , y la acción predeterminada es handler() , por lo que lo anterior es el equivalente de lo siguiente:
http://www.cleverna.me/index/action/handler
Si tiene un controlador de ruta article , lo solicitará así:
http://www.cleverna.me/article
En lugar de cadenas de consulta, el ciudadano pasa parámetros de URL que consisten en pares de nombre/valor. Si tuviera que aprobar una ID de artículo de 237 y un número de página de 2, agregaría pares de nombre/valor a la URL:
http://www.cleverna.me/article/id/237/page/2
Los nombres de parámetros válidos pueden contener letras, números, subrayos y guiones, pero deben comenzar con una letra o un guión bajo.
La acción del controlador predeterminada es handler() , pero puede especificar acciones alternativas con el parámetro action (más sobre esto más adelante):
http://www.cleverna.me/article/action/edit
Citizen también le permite insertar opcionalmente contenido relevante en sus URL, como así:
http://www.cleverna.me/article/My-Clever-Article-Title/page/2
Este contenido de SEO siempre debe seguir el nombre del controlador y preceder a los pares de nombre/valor, incluida la acción del controlador. Puede acceder a él genéricamente a través de route.descriptor o dentro del alcance url ( url.article en este caso), lo que significa que puede usarlo como un identificador único (más sobre los parámetros de URL en la sección de controladores de ruta).
La action de los parámetros de URL y direct están reservados para el marco, así que no los use para su aplicación.
Citizen se basa en una simple convención de controlador de visión modelo. El patrón del artículo mencionado anteriormente podría usar la siguiente estructura:
app/
controllers/
routes/
article.js
models/
article.js // Optional, name it whatever you want
views/
article.html // The default view file name should match the controller name
Se requiere al menos un controlador de ruta para una URL dada, y el archivo de vista predeterminado de un controlador de ruta debe compartir su nombre. Los modelos son opcionales.
Todas las vistas para un controlador de ruta dado pueden existir en la app/views/ directorio, o pueden colocarse en un directorio cuyo nombre coincide con el controlador para la organización más limpia:
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html // The default view
edit.html // Alternate article views
delete.html
Más sobre vistas en la sección Vistas.
Los modelos y vistas son opcionales y no necesariamente necesitan asociarse con un controlador particular. Si su controlador de ruta va a pasar su salida a otro controlador para su posterior procesamiento y representación final, no necesita incluir una vista coincidente (consulte la siguiente directiva del controlador).
Un controlador de ruta ciudadana es solo un módulo JavaScript. Cada controlador de ruta requiere al menos una exportación para servir como una acción para la ruta solicitada. La acción predeterminada debe llamarse handler() , que el ciudadano llama cuando no se especifica ninguna acción en la URL.
// Default route controller action
export const handler = async ( params , request , response , context ) => {
// Do some stuff
return {
// Send content and directives to the server
}
} El servidor Citizen llama handler() después de que procesa la solicitud inicial y la pasa 4 argumentos: un objeto params que contiene los parámetros de la solicitud, los objetos request y response node.js, y el contexto de la solicitud actual.
params config | La configuración de su aplicación, incluidas las personalizaciones para la acción del controlador actual |
route | Detalles de la ruta solicitada, como la URL y el nombre del controlador de ruta |
url | Cualquier parámetro derivado de la URL |
form | Datos recopilados de una publicación |
payload | La carga útil de la solicitud sin procesar |
cookie | Cookies enviadas con la solicitud |
session | Variables de sesión, si las sesiones están habilitadas |
Además de tener acceso a estos objetos dentro de su controlador, también se incluyen en su contexto de vista automáticamente para que pueda hacer referencia a sus plantillas de vista como variables locales (más detalles en la sección Vistas).
Por ejemplo, basado en la URL del artículo anterior ...
http://www.cleverna.me/article/My-Clever-Article-Title/id/237/page/2
... tendrá los siguientes params.url .
{
article : 'My-Clever-Article-Title' ,
id : '237' ,
page : '2'
} El nombre del controlador se convierte en una propiedad en el alcance de URL que hace referencia al descriptor, lo que lo hace bien adecuado para su uso como un identificador único. También está disponible en el objeto params.route como params.route.descriptor .
El argumento context contiene cualquier datos o directivas que hayan generado controladores anteriores en la cadena utilizando su declaración return .
Para devolver los resultados de la acción del controlador, incluya una declaración return con cualquier datos y directivas que desee pasar a Citizen.
Usando los parámetros de URL anteriores, puedo recuperar el contenido del artículo del modelo y volver al servidor:
// article controller
export const handler = async ( params ) => {
// Get the article
const article = await app . models . article . get ( {
article : params . url . article ,
page : params . url . page
} )
const author = await app . models . article . getAuthor ( {
author : article . author
} )
// Any data you want available to the view should be placed in the local directive
return {
local : {
article : article ,
author : author
}
}
} Se pueden solicitar acciones alternativas utilizando el parámetro URL action . Por ejemplo, tal vez queramos una acción y una visión diferentes para editar un artículo:
// http://www.cleverna.me/article/My-Clever-Article-Title/id/237/page/2/action/edit
// article controller
export const handler = async ( params ) => {
// Get the article
const article = await app . models . article . get ( {
article : params . url . article ,
page : params . url . page
} )
const author = await app . models . article . getAuthor ( {
author : article . author
} )
// Return the article for view rendering using the local directive
return {
local : {
article : article ,
author : author
}
}
}
export const edit = async ( params ) => {
// Get the article
const article = await app . models . article . get ( {
article : params . url . article ,
page : params . url . page
} )
// Use the /views/article/edit.html view for this action
return {
local : {
article : article
} ,
view : 'edit'
}
} Usted coloca los datos que desee transmitir al ciudadano dentro de la declaración return . Todos los datos que desea que se presente en su opinión deben pasar a los ciudadanos dentro de un objeto llamado local , como se muestra arriba. Se pueden pasar objetos adicionales al ciudadano para establecer directivas que proporcionen instrucciones al servidor (consulte las directivas del controlador). Incluso puede agregar sus propios objetos al contexto y pasarlos del controlador al controlador (más en la sección de encadenamiento del controlador).
Los modelos son módulos opcionales y su estructura depende completamente de usted. El ciudadano no habla directamente con sus modelos; Solo los almacena en app.models para su conveniencia. También puede importarlos manualmente en sus controladores si lo prefiere.
Se puede acceder a la siguiente función, cuando se coloca en app/models/article.js , en su aplicación a través de app.models.article.get() ::
// app.models.article.get()
export const get = async ( id ) => {
let article = // do some stuff to retrieve the article from the db using the provided ID, then...
return article
} Citizen utiliza literales de plantilla para la representación de vista por defecto. Puede instalar consolidate.js y usar cualquier motor de plantilla compatible. Simplemente actualice la configuración de configuración templateEngine en consecuencia.
En article.html , puede referencia a las variables que colocó dentro del objeto local pasó a la declaración de devolución del controlador de ruta. Citizen también inyecta propiedades del objeto params en su contexto de vista automáticamente, por lo que tiene acceso a esos objetos como variables locales (como el alcance de url ):
<!-- article.html -->
<!doctype html >
< html >
< body >
< main >
< h1 >
${local.article.title} — Page ${url.page}
</ h1 >
< h2 > ${local.author.name}, ${local.article.published} </ h2 >
< p >
${local.article.summary}
</ p >
< section >
${local.article.text}
</ section >
</ main >
</ body >
</ html > Por defecto, el servidor representa la vista cuyo nombre coincide con el del controlador. Para representar una vista diferente, use la Directiva view en su declaración de devolución.
Todas las vistas entran en /app/views . Si un controlador tiene múltiples vistas, puede organizarlas dentro de un directorio que lleva el nombre de ese controlador.
app/
controllers/
routes/
article.js
index.js
views/
article/
article.html // Default article controller view
edit.html
index.html // Default index controller view
Puede decirle a un controlador de ruta que devuelva sus variables locales como JSON o JSON-P estableciendo el encabezado HTTP Accept apropiado en su solicitud, permitiendo que el mismo recurso sirva tanto una vista HTML completa como JSON para las solicitudes AJAX y las API RESTful.
La acción handler() volvería:
{
"article" : {
"title" : " My Clever Article Title " ,
"summary" : " Am I not terribly clever? " ,
"text" : " This is my article text. "
},
"author" : {
"name" : " John Smith " ,
"email" : " [email protected] "
}
} Lo que haya agregado a la declaración de devolución del controlador se devolverá el objeto local .
Para JSONP, use callback en la URL:
http://www.cleverna.me/article/My-Clever-Article-Title/callback/foo
Devoluciones:
foo ( {
"article" : {
"title" : "My Clever Article Title" ,
"summary" : "Am I not terribly clever?" ,
"text" : "This is my article text."
} ,
"author" : {
"name" : "John Smith" ,
"email" : "[email protected]"
}
} ) ; Para forzar un tipo de contenido específico para una solicitud determinada, establezca response.contentType ContentType en el controlador de ruta a su salida deseada:
export const handler = async ( params , request , response ) => {
// Every request will receive a JSON response regardless of the Accept header
response . contentType = 'application/json'
}Puede forzar un tipo de respuesta global en todas las solicitudes dentro de un gancho de eventos.
Los ayudantes son módulos de utilidad opcionales y su estructura depende completamente de usted. Se almacenan en app.helpers . También puede importarlos manualmente en sus controladores y modelos si lo prefiere.
Se puede acceder a la siguiente función, cuando se coloque en app/helpers/validate.js , en su aplicación a través de app.helpers.validate.email() ::
// app.helpers.validate.email()
export const email = ( address ) => {
const emailRegex = new RegExp ( / [a-z0-9!##$%&''*+/=?^_`{|}~-]+(?:.[a-z0-9!##$%&''*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])? / i )
return emailRegex . test ( address )
} Citizen almacena todos los módulos en el alcance de la app no solo para una fácil recuperación, sino para admitir el reemplazo del módulo caliente (HMR). Cuando guarda cambios en cualquier módulo o vista en el modo de desarrollo, Citizen borra la importación del módulo existente y vuelve a implementar ese módulo en tiempo real.
Verá un registro de consola que observa el archivo afectado, y su aplicación continuará ejecutándose. No hay necesidad de reiniciar.
Citizen hace todo lo posible para manejar los errores con gracia sin salir del proceso. La siguiente acción del controlador lanzará un error, pero el servidor responderá con un 500 y seguirá funcionando:
export const handler = async ( params ) => {
// app.models.article.foo() doesn't exist, so this action will throw an error
const foo = await app . models . article . foo ( params . url . article )
return {
local : foo
}
}También puede lanzar un error manualmente y personalizar el mensaje de error:
export const handler = async ( params ) => {
// Get the article
const article = await app . models . article . get ( {
article : params . url . article ,
page : params . url . page
} )
// If the article exists, return it
if ( article ) {
return {
local : {
article : article
}
}
// If the article doesn't exist, throw a 404
} else {
// Error messages default to the standard HTTP Status Code response, but you can customize them.
let err = new Error ( 'The requested article does not exist.' )
// The HTTP status code defaults to 500, but you can specify your own
err . statusCode = 404
throw err
}
} Tenga en cuenta que params.route.controller se actualiza desde el controlador solicitado al error , por lo que cualquier referencia en su aplicación al controlador solicitado debe tener esto en cuenta.
Los errores se devuelven en el formato solicitado por la ruta. Si solicita JSON y la ruta arroja un error, Citizen devolverá el error en formato JSON.
El esqueleto de la aplicación creado por la utilidad de andamio incluye plantillas de vista de error opcional para errores comunes del cliente y servidor, pero puede crear plantillas para cualquier código de error HTTP.
El método de manejo de errores predeterminados de Citizen es capture , que intenta una recuperación elegante. Si prefiere salir del proceso después de un error, cambie config.citizen.errors para exit .
// config file: exit the process after an error
{
"citizen" : {
"errors" : "exit"
}
}Después de disparar el controlador de errores de aplicación, Citizen saldrá del proceso.
Para crear vistas de error personalizadas para los errores del servidor, cree un directorio llamado /app/views/error y lléntelo con plantillas que llevan el nombre del código de respuesta HTTP o el código de error de nodo.
app/
views/
error/
500.html // Displays any 500-level error
404.html // Displays 404 errors specifically
ENOENT.html // Displays bad file read operations
error.html // Displays any error without its own template
Además de ver los datos, la declaración de devolución de la acción del controlador de ruta también puede aprobar directivas para representar vistas alternativas, establecer cookies y variables de sesión, iniciar redireccionamientos, llamar y renderizar incluye, las acciones/vistas del controlador de ruta de caché (o solicitudes completas) e entregar la solicitud a otro controlador para un procesamiento adicional.
Por defecto, el servidor representa la vista cuyo nombre coincide con el del controlador. Para representar una vista diferente, use la Directiva view en su declaración de devolución:
// article controller
export const edit = async ( params ) => {
const article = await app . models . article . get ( {
article : params . url . article ,
page : params . url . page
} )
return {
local : article ,
// This tells the server to render app/views/article/edit.html
view : 'edit'
}
} Establece cookies devolviendo un objeto cookie dentro de la acción del controlador.
export const handler = async ( params ) => {
return {
cookie : {
// Cookie shorthand sets a cookie called username using the default cookie properties
username : params . form . username ,
// Sets a cookie called last_active that expires in 20 minutes
last_active : {
value : new Date ( ) . toISOString ( ) ,
expires : 20
}
}
}
}Aquí hay un ejemplo de la configuración predeterminada de un objeto de cookie completo:
myCookie = {
value : 'myValue' ,
// Valid expiration options are:
// 'now' - deletes an existing cookie
// 'never' - current time plus 30 years, so effectively never
// 'session' - expires at the end of the browser session (default)
// [time in minutes] - expires this many minutes from now
expires : 'session' ,
path : '/' ,
// citizen's cookies are accessible via HTTP/HTTPS only by default. To access a
// cookie via JavaScript, set this to false.
httpOnly : true ,
// Cookies are insecure when set over HTTP and secure when set over HTTPS.
// You can override that behavior globally with the https.secureCookies setting
// in your config or on a case-by-case basis with this setting.
secure : false
} Una vez que las cookies están configuradas en el cliente, están disponibles en params.cookie dentro de los controladores y simplemente cookie dentro de la vista:
<!doctype html >
< html >
< body >
< section >
Welcome, ${cookie.username}.
</ section >
</ body >
</ html > Las variables de cookies que colocas dentro de tu controlador no están disponibles de inmediato dentro del alcance params.cookie . Citizen debe recibir el contexto del controlador y enviar primero la respuesta al cliente, por lo que usa una instancia local de la variable si necesita acceder a él durante la misma solicitud.
Todas las cookies establecidas por Citizen comienzan con el prefijo ctzn_ para evitar colisiones. No comience los nombres de sus cookies con ctzn_ y no debería tener problemas.
Si usa Citizen detrás de un proxy, como Nginx o Apache, asegúrese de tener un encabezado Forwarded HTTP en la configuración de su servidor para que el manejo de cookies seguras de Citizen funcione correctamente.
Aquí hay un ejemplo de cómo podría establecer esto en Nginx:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:8080;
}
Si las sesiones están habilitadas, puede acceder a las variables de sesión a través de params.session en su controlador o simplemente session dentro de las vistas. Estos ámbitos locales hacen referencia a la sesión del usuario actual sin tener que aprobar una ID de sesión.
Por defecto, una sesión tiene cuatro propiedades: id , started , expires y timer . La ID de sesión también se envía al cliente como una cookie llamada ctzn_session_id .
Configurar las variables de sesión es más o menos lo mismo que establecer variables de cookies:
return {
session : {
username : 'Danny' ,
nickname : 'Doc'
}
} Al igual que las cookies, las variables de sesión que acaba de asignar no están disponibles durante la misma solicitud dentro del alcance params.session , así que use una instancia local si necesita acceder a estos datos de inmediato.
Las sesiones caducan en función de la propiedad de configuración sessions.lifespan , que representa la duración de una sesión en minutos. El valor predeterminado es de 20 minutos. El timer se restablece con cada solicitud del usuario. Cuando se agota el timer , se elimina la sesión. Cualquier solicitud del cliente después de ese tiempo generará una nueva ID de sesión y enviará una nueva cookie de ID de sesión al cliente.
Para borrar y expirar por la fuerza la sesión actual del usuario:
return {
session : {
expires : 'now'
}
} Todas las variables de sesión establecidas por Citizen comienzan con el prefijo ctzn_ para evitar colisiones. No comience los nombres de variables de su sesión con ctzn_ y no debería tener problemas.
Puede pasar instrucciones de redirección al servidor que se iniciará después de procesar la acción del controlador.
El objeto redirect toma una cadena de URL en su versión de taquigrafía, o tres opciones: statusCode , url y refresh . Si no proporciona un código de estado, Citizen usa 302 (redirección temporal). La opción refresh determina si la redirección utiliza un encabezado de ubicación o el encabezado de actualización no estándar.
// Initiate a temporary redirect using the Location header
return {
redirect : '/login'
}
// Initiate a permanent redirect using the Refresh header, delaying the redirect by 5 seconds
return {
redirect : {
url : '/new-url' ,
statusCode : 301 ,
refresh : 5
}
} A diferencia del encabezado de ubicación, si usa la opción refresh , Citizen enviará una vista renderizada al cliente porque la redirección ocurre con el lado del cliente.
Usando el encabezado de ubicación rompe (en mi opinión) el encabezado de referente porque el referente termina no siendo el recurso que inició la redirección, sino el recurso anterior a la página que lo inició. Para evitar este problema, Citizen almacena una variable de sesión llamada ctzn_referer que contiene la URL del recurso que inició la redirección, que puede usar para redirigir a los usuarios correctamente. Por ejemplo, si un usuario no autenticado intenta acceder a una página segura y los redirige a un formulario de inicio de sesión, la dirección de la página segura se almacenará en ctzn_referer para que pueda enviarlos allí en lugar de la página anterior.
Si no ha habilitado las sesiones, Citizen regresa a crear una cookie llamada ctzn_referer .
Si usa Citizen detrás de un proxy, como Nginx o Apache, asegúrese de tener un encabezado Forwarded HTTP en la configuración de su servidor, por lo que ctzn_referer funciona correctamente.
Aquí hay un ejemplo de cómo podría establecer esto en Nginx:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:8080;
}
Puede configurar los encabezados HTTP utilizando la Directiva header :
return {
header : {
'Cache-Control' : 'max-age=86400' ,
'Date' : new Date ( ) . toISOString ( )
}
} También puede establecer encabezados directamente usando el método response.setHeader() de Node, pero utilizando la Directiva header de Citizen preserva esos encabezados en el caché de solicitud, por lo que se aplicarán cada vez que esa acción del controlador se extraiga del caché.
Citizen le permite utilizar patrones de MVC completos, según lo incluyen, que son la versión de los componentes de Citizen. Cada uno tiene su propio controlador de ruta, modelo y vista. Incluye se puede utilizar para realizar una acción o devolver una vista completa. Cualquier controlador de ruta puede ser una inclusión.
Digamos que la plantilla de nuestro patrón de artículo tiene el siguiente contenido. La sección principal contiene meta datos dinámicos, y el contenido del encabezado cambia dependiendo de si el usuario ha iniciado sesión o no:
<!doctype html >
< html >
< head >
< title > ${local.metaData.title} </ title >
< meta name =" description " content =" ${local.metaData.description} " >
< meta name =" keywords " content =" ${local.metaData.keywords} " >
< link rel =" stylesheet " type =" text/css " href =" site.css " >
</ head >
< body >
< header >
${ cookie.username ? ' < p > Welcome, ' + cookie.username + ' </ p > ' : ' < a href =" /login " > Login </ a > ' }
</ header >
< main >
< h1 > ${local.article.title} — Page ${url.page} </ h1 >
< p > ${local.article.summary} </ p >
< section > ${local.article.text} </ section >
</ main >
</ body >
</ html >Probablemente tenga sentido usar incluye para la sección y el encabezado de la cabeza porque usará ese código en todas partes, pero en lugar de parciales simples, puede crear Citizen INCONDE. La sección principal puede usar su propio modelo para poblar los meta datos, y dado que el encabezado es diferente para los usuarios autenticados, extraemos esa lógica de la vista y lo coloquen en el controlador del encabezado. Me gusta seguir la convención de comenzar parciales con un bajo, pero eso depende de ti:
app/
controllers/
routes/
_head.js
_header.js
article.js
models/
_head.js
article.js
views/
_head.html
_header/
_header.html
_header-authenticated.html // A different header for logged in users
article.html
Cuando se dispara el controlador del artículo, tiene que decirle a Citizen que incluye las necesidades de TI. Hacemos eso con la Directiva include :
// article controller
export const handler = async ( params ) => {
// Get the article
const article = await app . models . article . get ( {
article : params . url . article ,
page : params . url . page
} )
return {
local : {
article : article
} ,
include : {
// Include shorthand is a string containing the pathname to the desired route controller
_head : '/_head/action/article' ,
// Long-form include notation can explicitly define a route controller, action, and view
_header : {
controller : '_header' ,
// If the username cookie exists, use the authenticated action. If not, use the default action.
action : params . cookie . username ? 'authenticated' : 'handler'
}
}
}
} Los ciudadanos incluyen patrones tienen los mismos requisitos que los patrones regulares, incluido un controlador con una acción pública. La Directiva include anterior le dice a Citizen que llame a los controladores _head y _header , los pase los mismos argumentos que se pasaron al controlador article (parámetros, solicitud, respuesta, contexto), representaron sus respectivas opiniones y agregan las vistas resultantes al contexto de la visión.
Así es como podría verse nuestro controlador de la sección principal:
// _head controller
export const article = async ( params ) => {
let metaData = await app . models . _head ( { article : params . url . article } )
return {
local : {
metaData : metaData
}
}
}Y la vista de la sección de la cabeza:
< head >
< title > ${local.metaData.title} </ title >
< meta name =" description " content =" ${local.metaData.description} " >
< meta name =" keywords " content =" ${local.metaData.keywords} " >
< link rel =" stylesheet " type =" text/css " href =" site.css " >
</ head >Así es como podría verse nuestro controlador de encabezado:
// _header controller
// No need for a return statement, and no need to specify the view
// because handler() renders the default view.
//
// Every route controller needs at least one action, even if it's empty.
export const handler = ( ) => { }
export const authenticated = ( ) => {
return {
view : '_header-authenticated'
}
}Y el encabezado vistas:
<!-- /views/_header/_header.html -->
< header >
< a href =" /login " > Login </ a >
</ header > <!-- /views/_header/_header-authenticated.html -->
< header >
< p > Welcome, ${cookie.username} </ p >
</ header > Las incluyas renderizadas se almacenan en el alcance include :
<!-- /views/article.html -->
<!doctype html >
< html >
${include._head}
< body >
${include._header}
< main >
< h1 > ${local.title} — Page ${url.page} </ h1 >
< p > ${local.summary} </ p >
< section > ${local.text} </ section >
</ main >
</ body >
</ html >Citizen incluye son autónomos y entregados al controlador de llamadas como una vista totalmente renderizada. Si bien reciben los mismos datos (parámetros de URL, entradas de formulario, contexto de solicitud, etc.) Como el controlador de llamadas, los datos generados dentro de una inclusión no se vuelven a pasar a la persona que llama.
Se puede acceder a un patrón destinado a usarse como incluido a través de HTTP como cualquier otro controlador de ruta. Puede solicitar el controlador _header como SO y recibir una parte de HTML o JSON como respuesta:
http://cleverna.me/_header
Esto es excelente para manejar el primer lado del servidor de solicitud y luego actualizar contenido con una biblioteca del lado del cliente.
Los ciudadanos incluyen proporcionar una rica funcionalidad, pero tienen limitaciones y pueden ser exageradas en ciertas situaciones.
Citizen le permite encadenar múltiples controladores de ruta en serie desde una sola solicitud utilizando la next directiva. El controlador solicitado pasa sus datos y presenta una vista a un controlador posterior, agregando sus propios datos y haciendo su propia vista.
Puede unir tantos controladores de ruta juntos en una sola solicitud como desee. Cada controlador de ruta tendrá sus datos y la salida de la vista almacenada en el objeto params.route.chain .
// The index controller accepts the initial request and hands off execution to the article controller
export const handler = async ( params ) => {
let user = await app . models . user . getUser ( { userID : params . url . userID } )
return {
local : {
user : user
} ,
// Shorthand for next is a string containing the pathname to the route controller.
// URL paramaters in this route will be parsed and handed to the next controller.
next : '/article/My-Article/id/5'
// Or, you can be explicit, but without parameters
next : {
// Pass this request to app/controllers/routes/article.js
controller : 'article' ,
// Specifying the action is optional. The next controller will use its default action, handler(), unless you specify a different action here.
action : 'handler' ,
// Specifying the view is optional. The next controller will use its default view unless you tell it to use a different one.
view : 'article'
}
// You can also pass custom directives and data.
doSomething: true
}
}Cada controlador en la cadena tiene acceso al contexto y vistas del controlador anterior. El último controlador en la cadena proporciona la vista final renderizada. Un controlador de diseño con todos los elementos globales de su sitio es un uso común para esto.
// The article controller does its thing, then hands off execution to the _layout controller
export const handler = async ( params , request , response , context ) => {
let article = await getArticle ( { id : params . url . id } )
// The context from the previous controller is available to you in the current controller.
if ( context . doSomething ) { // Or, params.route.chain.index.context
await doSomething ( )
}
return {
local : {
article : article
} ,
next : '/_layout'
}
} The rendered view of each controller in the chain is stored in the route.chain object:
<!-- index.html, which is stored in route.chain.index.output -->
< h1 > Welcome, ${local.user.username}! </ h1 >
<!-- article.html, which is stored in route.chain.article.output -->
< h1 > ${local.article.title} </ h1 >
< p > ${local.article.summary} </ p >
< section > ${local.article.text} </ section >The layout controller handles the includes and renders its own view. Because it's the last controller in the chain, this rendered view is what will be sent to the client.
// _layout controller
export const handler = async ( params ) => {
return {
include : {
_head : '/_head' ,
_header : {
controller : '_header' ,
action : params . cookie . username ? 'authenticated' : 'handler'
} ,
_footer : '/_footer
}
}
} <!-- _layout.html -->
<!doctype html >
< html >
${include._head}
< body >
${include._header}
< main >
<!-- You can render each controller's view explicitly -->
${route.chain.index.output}
${route.chain.article.output}
<!-- Or, you can loop over the route.chain object to output the view from each controller in the chain -->
${Object.keys(route.chain).map( controller = > { return route.chain[controller].output }).join('')}
</ main >
${include._footer}
</ body >
</ html > You can skip rendering a controller's view in the chain by setting the view directive to false:
// This controller action won't render a view
export const handler = async ( ) => {
return {
view : false ,
next : '/_layout'
}
} To bypass next in a request, add /direct/true to the URL.
http://cleverna.me/index/direct/true
The requested route controller's next directive will be ignored and its view will be returned to the client directly.
As mentioned in the config section at the beginning of this document, you can specify a default layout controller in your config so you don't have to insert it at the end of every controller chain:
{
"citizen" : {
"layout" : {
"controller" : " _layout " ,
"view" : " _layout "
}
}
} If you use this method, there's no need to use next for the layout. The last controller in the chain will always hand the request to the layout controller for final rendering.
citizen provides several ways for you to improve your app's performance, most of which come at the cost of system resources (memory or CPU).
In many cases, a requested URL or route controller action will generate the same view every time based on the same input parameters, so it doesn't make sense to run the controller chain and render the view from scratch for each request. citizen provides flexible caching capabilities to speed up your server side rendering via the cache directive.
If a given request (URL) will result in the exact same rendered view with every request, you can cache that request with the request property. This is the fastest cache option because it pulls a fully rendered view from memory and skips all controller processing.
Let's say you chain the index, article, and layout controllers like we did above. If you put the following cache directive in your index controller, the requested URL's response will be cached and subsequent requests will skip the index, article, and layout controllers entirely.
return {
next : '/article' ,
cache : {
request : true
}
}For the request cache directive to work, it must be placed in the first controller in the chain; in other words, the original requested route controller (index in this case). It will be ignored in any subsequent controllers.
The URL serves as the cache key, so each of the following URLs would generate its own cache item:
http://cleverna.me/article
http://cleverna.me/article/My-Article
http://cleverna.me/article/My-Article/page/2
The example above is shorthand for default cache settings. The cache.request directive can also be an object with options:
// Cache the requested route with some additional options
return {
cache : {
request : {
// Optional. This setting lets the server respond with a 304 Not Modified
// status if the cache content hasn't been updated since the client last
// accessed the route. Defaults to the current time if not specified.
lastModified : new Date ( ) . toISOString ( ) ,
// Optional. List of valid URL parameters that protects against accidental
// caching of malformed URLs.
urlParams : [ 'article' , 'page' ] ,
// Optional. Life of cached item in minutes. Default is 15 minutes.
// For no expiration, set to 'application'.
lifespan : 15 ,
// Optional. Reset the cached item's expiration timer whenever the item is
// accessed, keeping it in the cache until traffic subsides. Default is true.
resetOnAccess : true
}
}
} If a given route chain will vary across requests, you can still cache individual controller actions to speed up rendering using the action property.
// Cache this controller action using the default settings
return {
cache : {
action : true
}
}
// Cache this controller with additional options
return {
cache : {
action : {
// These options function the same as request caching (see above)
urlParams : [ 'article' , 'page' ] ,
lifespan : 15 ,
resetOnAccess : true
}
}
}When you cache controller actions, their context is also cached. Setting a cookie or session variable in a cached controller action means all future requests for that action will set the same cookie or session variable—probably not something you want to do with user data.
lastModified This setting lets the server respond with a faster 304 Not Modified response if the content of the request cache hasn't changed since the client last accessed it. By default, it's set to the time at which the request was cached, but you can specify a custom date in ISO format that reflects the last modification to the request's content.
return {
next : '/_layout' ,
cache : {
request : {
// Use toISOString() to format your date appropriately
lastModified : myDate . toISOString ( ) // 2015-03-05T08:59:51.491Z
}
}
} urlParams The urlParams property helps protect against invalid cache items (or worse: an attack meant to flood your server's resources by overloading the cache).
return {
next : '/_layout' ,
cache : {
request : {
urlParams : [ 'article' , 'page' ]
}
}
}If we used the example above in our article controller, the following URLs would be cached because the "article" and "page" URL parameters are permitted:
http://cleverna.me/article
http://cleverna.me/article/My-Article-Title
http://cleverna.me/article/My-Article-Title/page/2
The following URLs wouldn't be cached, which is a good thing because it wouldn't take long for an attacker's script to loop over a URL and flood the cache:
http://cleverna.me/article/My-Article-Title/dosattack/1
http://cleverna.me/article/My-Article-Title/dosattack/2
http://cleverna.me/article/My-Article-Title/page/2/dosattack/3
The server logs a warning when invalid URL parameters are present, but continues processing without caching the result.
lifespanThis setting determines how long the request or controller action should remain in the cache, in minutes.
return {
cache : {
request : {
// This cached request will expire in 10 minutes
lifespan : 10
}
}
} resetOnAccess Used with the lifespan setting, resetOnAccess will reset the timer of the route or controller cache whenever it's accessed, keeping it in the cache until traffic subsides. Defaults to true .
return {
cache : {
request : {
// This cached request will expire in 10 minutes, but if a request accesses it
// before then, the cache timer will be reset to 10 minutes from now
lifespan : 10 ,
resetOnAccess : true
}
}
} In most cases, you'll probably want to choose between caching an entire request (URL) or caching individual controller actions, but not both.
When caching an include controller action, the route pathname pointing to that include is used as the cache key. If you use logic to render different views using the same controller action, the first rendered view will be cached. You can pass an additional URL parameter in such cases to get past this limitation and create a unique cache item for different include views.
export const handler = async ( context ) => {
return : {
// Two different versions of the _header include will be cached becaues the URLs are unique
include : context . authenticated ? '/_header/authenticated/true' : '/_header'
}
} citizen's cache is a RAM cache stored in the V8 heap, so be careful with your caching strategy. Use the lifespan and resetOnAccess options so URLs that receive a lot of traffic stay in the cache, while less popular URLs naturally fall out of the cache over time.
By caching static assets in memory, you speed up file serving considerably. To enable static asset caching for your app's public (web) directory, set cache.static.enabled to true in your config:
{
"citizen" : {
"cache" : {
"static" : {
"enabled" : true
}
}
}
}citizen handles response headers automatically (ETags, 304 status codes, etc.) using each file's last modified date. Note that if a file changes after it's been cached, you'll need to clear the file cache using cache.clear() or restart the app.
To clear a file from the cache in a running app:
app . cache . clear ( { file : '/absolute/path/to/file.jpg' } )With static caching enabled, all static files citizen serves will be cached in the V8 heap, so keep an eye on your app's memory usage to make sure you're not using too many resources.
citizen automatically sets ETag headers for cached requests and static assets. You don't need to do anything to make them work. The Cache-Control header is entirely manual, however.
To set the Cache-Control header for static assets, use the cache.control setting in your config:
{
"citizen" : {
"cache" : {
"static" : true ,
"control" : {
"/css/global.css" : " max-age=86400 " ,
"/css/index.css" : " max-age=86400 " ,
"/js/global.js" : " max-age=86400 " ,
"/js/index.js" : " max-age=86400 " ,
"/images/logo.png" : " max-age=31536000 "
}
}
}
} The key name is the pathname that points to the static asset in your web directory. If your app's URL path is /my/app , then this value should be something like /my/app/styles.css . The value is the Cache-Control header value you want to assign to that asset.
You can use strings that match the exact pathname like above, or you can also use wildcards. Mixing the two is fine:
{
"citizen" : {
"cache" : {
"static" : true ,
"control" : {
"/css/*" : " max-age=86400 " ,
"/js/*" : " max-age=86400 " ,
"/images/logo.png" : " max-age=31536000 "
}
}
}
}Here's a great tutorial on client-side caching to help explain ETag and Cache-Control headers.
Both dynamic routes and static assets can be compressed before sending them to the browser. To enable compression for clients that support it:
{
"citizen" : {
"compression" : {
"enabled" : true
}
}
}Proxies, firewalls, and other network circumstances can strip the request header that tells the server to provide compressed assets. You can force gzip or deflate for all clients like this:
{
"citizen" : {
"compression" : {
"enabled" : true ,
"force" : " gzip "
}
}
}If you have request caching enabled, both the original (identity) and compressed (gzip and deflate) versions of the request will be cached, so your cache's memory utilization will increase.
citizen includes basic request payload parsing. When a user submits a form, the parsed form data is available in your controller via params.form . If you want to use a third-party package to parse the form data yourself, you can disable form parsing in the config and access the raw payload via request.payload .
// login controller
export const handler = ( params ) => {
// Set some defaults for the login view
params . form . username = ''
params . form . password = ''
params . form . remember = false
}
// Using a separate action in your controller for form submissions is probably a good idea
export const submit = async ( params ) => {
let authenticate = await app . models . user . authenticate ( {
username : params . form . username ,
password : params . form . password
} ) ,
cookies = { }
if ( authenticate . success ) {
if ( params . form . remember ) {
cookies . username : authenticate . username
}
return {
cookies : cookies ,
redirect : '/'
}
} else {
return {
local : {
message : 'Login failed.'
}
}
}
}If it's a multipart form containing a file, the form object passed to your controller will look something like this:
{
field1 : 'bar' ,
field2 : 'buzz' ,
fileField1 : {
filename : 'file.png' ,
contentType : 'image/png' ,
binary : < binary data >
}
} File contents are presented in binary format, so you'll need to use Buffer.from(fileField1.binary, 'binary') to create the actual file for storage.
You can pass global form settings via citizen.form in the config or at the controller action level via controller config (see below).
Use the maxPayloadSize config to limit form uploads. The following config sets the maxFieldsSize to 512k:
{
"citizen" : {
"forms" : {
"maxPayloadSize" : 500000 // 0.5MB
}
}
} The maxPayloadSize option includes text inputs and files in a multipart form in its calculations. citizen throws an error if form data exceeds this amount.
Certain events will occur throughout the life of your citizen application, or within each request. You can act on these events, execute functions, set directives, and pass the results to the next event or controller via the context argument. For example, you might set a cookie at the beginning of every new session, or check for cookies at the beginning of every request and redirect the user to a login page if they're not authenticated.
To take advantage of these events, include a directory called "hooks" in your app with any or all of following modules and exports:
app/
controllers/
hooks/
application.js // exports start() and error()
request.js // exports start() and end()
response.js // exports start() and end()
session.js // exports start() and end()
request.start() , request.end() , and response.start() are called before your controller is fired, so the output from those events is passed from each one to the next, and ultimately to your controller via the context argument. Exactly what actions they perform and what they output—content, citizen directives, custom directives—is up to you.
All files and exports are optional. citizen parses them at startup and only calls them if they exist. For example, you could have only a request.js module that exports start() .
Here's an example of a request module that checks for a username cookie at the beginning of every request and redirects the user to the login page if it doesn't exist. We also avoid a redirect loop by making sure the requested controller isn't the login controller:
// app/controllers/hooks/request.js
export const start = ( params ) => {
if ( ! params . cookie . username && params . route . controller !== 'login' ) {
return {
redirect = '/login'
}
}
} session.end is slightly different in terms of the arguments it receives, which consists only of a copy of the expired session (no longer active):
// app/controllers/hooks/session.js
export const end = ( expiredSession ) => {
// do something whenever a session ends
} By default, all controllers respond to requests from the host only. citizen supports cross-domain HTTP requests via access control headers.
To enable cross-domain access for individual controller actions, add a cors object with the necessary headers to your controller's exports:
export const config = {
// Each controller action can have its own CORS headers
handler : {
cors : {
'Access-Control-Allow-Origin' : 'http://www.foreignhost.com' ,
'Access-Control-Expose-Headers' : 'X-My-Custom-Header, X-Another-Custom-Header' ,
'Access-Control-Max-Age' : 600 ,
'Access-Control-Allow-Credentials' : 'true' ,
'Access-Control-Allow-Methods' : 'OPTIONS, PUT' ,
'Access-Control-Allow-Headers' : 'Content-Type' ,
'Vary' : 'Origin'
}
}
} Why not just use the HTTP Headers directive or set them manually with response.setHeader() ? When citizen receives a request from an origin other than the host, it checks for the cors export in your controller to provide a preflight response without you having to write your own logic within the controller action. You can of course check request.method and write logic to handle this manually if you prefer.
For more details on CORS, check out the W3C spec and the Mozilla Developer Network.
If you use citizen behind a proxy, such as NGINX or Apache, make sure you have a Forwarded header in your server configuration so citizen handles CORS requests correctly. Different protocols (HTTPS on your load balancer and HTTP in your citizen app) will cause CORS requests to fail without these headers.
Here's an example of how you might set this up in NGINX:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:3000;
}
citizen has a built-in application cache where you can store basically anything: strings, objects, buffers, static files, etc.
You can store any object in citizen's cache. The benefits of using cache over storing content in your own global app variables are built-in cache expiration and extension, as well as wrappers for reading, parsing, and storing file content.
citizen's default cache time is 15 minutes, which you can change in the config (see Configuration). Cached item lifespans are extended whenever they're accessed unless you pass resetOnAccess: false or change that setting in the config.
// Cache a string in the default app scope for 15 minutes (default). Keys
// must be unique within a given scope.
app . cache . set ( {
key : 'welcome-message' ,
value : 'Welcome to my site.'
} )
// Cache a string under a custom scope, which is used for retrieving or clearing
// multiple cache items at once. Keys must be unique within a given scope.
// Reserved scope names are "app", "routes", and "files".
app . cache . set ( {
key : 'welcome-message' ,
scope : 'site-messages' ,
value : 'Welcome to our site.'
} )
// Cache a string for the life of the application.
app . cache . set ( {
key : 'welcome-message' ,
value : 'Welcome to my site.' ,
lifespan : 'application'
} )
// Cache a file buffer using the file path as the key. This is a wrapper for
// fs.readFile and fs.readFileSync paired with citizen's cache function.
// Optionally, tell citizen to perform a synchronous file read operation and
// use an encoding different from the default (UTF-8).
app . cache . set ( {
file : '/path/to/articles.txt' ,
synchronous : true ,
encoding : 'CP-1252'
} )
// Cache a file with a custom key. Optionally, parse the JSON and store the
// parsed object in the cache instead of the raw buffer. Expire the cache
// after 10 minutes, regardless of whether the cache is accessed or not.
app . cache . set ( {
file : '/path/to/articles.json' ,
key : 'articles' ,
parseJSON : true ,
lifespan : 10 ,
resetOnAccess : false
} ) app , routes , and files are reserved scope names, so you can't use them for your own custom scopes.
This is a way to check for the existence of a given key or scope in the cache without resetting the cache timer on that item. Returns false if a match isn't found.
// Check for the existence of the specified key
let keyExists = app . cache . exists ( { key : 'welcome-message' } ) // keyExists is true
let keyExists = app . cache . exists ( { file : '/path/to/articles.txt' } ) // keyExists is true
let keyExists = app . cache . exists ( { file : 'articles' } ) // keyExists is true
let keyExists = app . cache . exists ( { key : 'foo' } ) // keyExists is false
// Check the specified scope for the specified key
let keyExists = app . cache . exists ( {
scope : 'site-messages' ,
key : 'welcome-message'
} )
// keyExists is true
// Check if the specified scope exists and contains items
let scopeExists = app . cache . exists ( {
scope : 'site-messages'
} )
// scopeExists is true
// Check if the route cache has any instances of the specified route
let controllerExists = app . cache . exists ( {
route : '/article'
} ) Retrieve an individual key or an entire scope. Returns false if the requested item doesn't exist. If resetOnAccess was true when the item was cached, using retrieve() will reset the cache clock and extend the life of the cached item. If a scope is retrieved, all items in that scope will have their cache timers reset.
Optionally, you can override the resetOnAccess attribute when retrieving a cache item by specifying it inline.
// Retrieve the specified key from the default (app) scope
let welcomeMessage = app . cache . get ( {
key : 'welcome-message'
} )
// Retrieve the specified key from the specified scope and reset its cache timer
// even if resetOnAccess was initially set to false when it was stored
let welcomeMessage = app . cache . get ( {
scope : 'site-messages' ,
key : 'welcome-message' ,
resetOnAccess : true
} )
// Retrieve all keys from the specified scope
let siteMessages = app . cache . get ( {
scope : 'site-messages'
} )
// Retrieve a cached file
let articles = app . cache . get ( {
file : '/path/to/articles.txt'
} )
// Retrieve a cached file with its custom key
let articles = app . cache . get ( {
file : 'articles'
} )Clear a cache object using a key or a scope.
// Store some cache items
app . cache . set ( {
key : 'welcome-message' ,
scope : 'site-messages' ,
value : 'Welcome to our site.'
} )
app . cache . set ( {
key : 'goodbye-message' ,
scope : 'site-messages' ,
value : 'Thanks for visiting!'
} )
app . cache . set ( {
file : '/path/to/articles.txt' ,
synchronous : true
} )
// Clear the welcome message from its custom scope cache
app . cache . clear ( { scope : 'site-messages' , key : 'welcome-message' } )
// Clear all messages from the cache using their custom scope
app . cache . clear ( { scope : 'site-messages' } )
// Clear the articles cache from the file scope
app . cache . clear ( { file : '/path/to/articles.txt' } ) cache.clear() can also be used to delete cached requests and controller actions.
app . cache . clear ( {
route : '/article/My-Article/page/2'
} )
// Clear the entire route scope
app . cache . clear ( { scope : 'routes' } )
// Clear the entire file scope
app . cache . clear ( { scope : 'files' } )
// Clear the entire cache
app . cache . clear ( ) citizen's log() function is exposed for use in your app via app.log() .
Makes it easy to log comments to either the console or a file (or both) in a way that's dependent on the mode of the framework.
When citizen is in production mode, log() does nothing by default. In development mode, log() will log whatever you pass to it. This means you can place it throughout your application's code and it will only write to the log in development mode. You can override this behavior globally with the log settings in your config file or inline with the console or file options when calling log() .
app . log ( {
// Optional. Valid settings are "status" (default) or "error".
type : 'status' ,
// Optional string. Applies a label to your log item.
label : 'Log output' ,
// The content of your log. If it's anything other than a string or
// number, log() will run util.inspect on it and dump the contents.
contents : someObject ,
// Optional. Enables console logs.
console : true ,
// Optional. Enables file logging.
file : false ,
// Optional. File name you'd like to write your log to.
file : 'my-log-file.log' ,
// Optional. Disables the timestamp that normally appears in front of the log
timestamp : false
} ) Log files appear in the directory you specify in config.citizen.directories.logs .
Warning: development mode is inherently insecure. Don't use it in a production environment.
If you set "mode": "development" in your config file, citizen dumps all major operations to the console.
You can also dump the request context to the view by setting development.debug.view in your config file to true , or use the ctzn_debug URL parameter on a per-request basis:
// config file: always dumps debug output in the view
{
"citizen" : {
"development" : {
"debug" : {
"view" : true
}
}
}
} By default, citizen dumps the pattern's complete context. You can specify the exact object to debug with the ctzn_inspect URL parameter:
// Dumps the server params object
http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_inspect/params
// Dumps the user's session scope
http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_inspect/params.session
The debug output traverses objects 4 levels deep by default. To display deeper output, use the development.debug.depth setting in your config file or append ctzn_debugDepth to the URL. Debug rendering will take longer the deeper you go.
// config file: debug 4 levels deep
{
"citizen" : {
"development" : {
"debug" : {
"depth" : 6
}
}
}
}
// URL
// http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_debugDepth/4 In development mode, you must specify the ctzn_debug URL parameter to display debug output. Debug output is disabled in production mode.
The util directory within the citizen package has some helpful utilities.
Creates a complete skeleton of a citizen app with a functional index pattern and error templates.
$ node node_modules/citizen/util/scaffold skeletonResulting file structure:
app/
config/
citizen.json
controllers/
hooks/
application.js
request.js
response.js
session.js
routes/
index.js
models/
index.js
views/
error/
404.html
500.html
ENOENT.html
error.html
index.html
start.js
web/
Run node node_modules/citizen/util/scaffold skeleton -h for options.
Creates a complete citizen MVC pattern. The pattern command takes a pattern name and options:
$ node node_modules/citizen/util/scaffold pattern [options] [pattern] For example, node scaffold pattern article will create the following pattern:
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html
Use node node_modules/citizen/util/scaffold pattern -h to see all available options for customizing your patterns.
(The MIT License)
Copyright (c) 2014-2024 Jay Sylvester
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
El software se proporciona 'tal cual', sin garantía de ningún tipo, expresa o implícita, incluidas, entre otros, las garantías de comerciabilidad, idoneidad para un propósito particular y no infracción. En ningún caso los autores o titulares de derechos de autor serán responsables de cualquier reclamo, daños u otra responsabilidad, ya sea en una acción de contrato, agravio o de otra manera, que surge, de o en relación con el software o el uso u otros tratos en el software.
By default, citizen dumps the pattern's complete context. You can specify the exact object to debug with the ctzn_inspect URL parameter:
// Dumps the server params object
http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_inspect/params
// Dumps the user's session scope
http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_inspect/params.session
The debug output traverses objects 4 levels deep by default. To display deeper output, use the development.debug.depth setting in your config file or append ctzn_debugDepth to the URL. Debug rendering will take longer the deeper you go.
// config file: debug 4 levels deep
{
"citizen" : {
"development" : {
"debug" : {
"depth" : 6
}
}
}
}
// URL
// http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_debugDepth/4 In development mode, you must specify the ctzn_debug URL parameter to display debug output. Debug output is disabled in production mode.
The util directory within the citizen package has some helpful utilities.
Creates a complete skeleton of a citizen app with a functional index pattern and error templates.
$ node node_modules/citizen/util/scaffold skeletonResulting file structure:
app/
config/
citizen.json
controllers/
hooks/
application.js
request.js
response.js
session.js
routes/
index.js
models/
index.js
views/
error/
404.html
500.html
ENOENT.html
error.html
index.html
start.js
web/
Run node node_modules/citizen/util/scaffold skeleton -h for options.
Creates a complete citizen MVC pattern. The pattern command takes a pattern name and options:
$ node node_modules/citizen/util/scaffold pattern [options] [pattern] For example, node scaffold pattern article will create the following pattern:
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html
Use node node_modules/citizen/util/scaffold pattern -h to see all available options for customizing your patterns.
(The MIT License)
Copyright (c) 2014-2024 Jay Sylvester
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
El software se proporciona 'tal cual', sin garantía de ningún tipo, expresa o implícita, incluidas, entre otros, las garantías de comerciabilidad, idoneidad para un propósito particular y no infracción. En ningún caso los autores o titulares de derechos de autor serán responsables de cualquier reclamo, daños u otra responsabilidad, ya sea en una acción de contrato, agravio o de otra manera, que surge, de o en relación con el software o el uso u otros tratos en el software.