O Citizen é uma estrutura de aplicativos da Web baseada em MVC, projetada para pessoas interessadas em construir rapidamente sites escalonáveis e escaláveis, em vez de cavar as tripas de Node ou montar uma torre jenga queimada feita de 50 pacotes diferentes.
Use o Citizen como base para um aplicativo da Web tradicional do servidor, um aplicativo modular de uma página única (SPA) ou uma API RESTful.
Houve inúmeras mudanças de ruptura na transição de 0.9.x para 1.0.x. Consulte o Changelog para obter uma lista detalhada e revise bem esta documentação atualizada.
Claramente, isso é muito mais conteúdo do que qualquer readme NPM/GitHub deve conter. Estou trabalhando em um site para esta documentação.
Eu uso o Citizen no meu site pessoal e o originaltrilogy.com. O OT.com lida com uma quantidade moderada de tráfego (algumas centenas de milhares de visualizações a cada mês) em um plano de hospedagem em nuvem de US $ 30 executando uma única instância do Citizen, onde o aplicativo/processo funciona por meses seguidos sem travar. É muito estável.
Esses comandos criarão um novo diretório para o seu aplicativo da web, instalarão o Citizen, usarão seu utilitário de andaimes para criar o esqueleto do aplicativo e iniciar o servidor da web:
$ mkdir myapp && cd myapp
$ npm install citizen
$ node node_modules/citizen/util/scaffold skeleton
$ node app/start.jsSe tudo correr bem, você verá a confirmação no console de que o servidor da Web está em execução. Vá para http://127.0.0.1:3000 no seu navegador e você verá um modelo de índice nu.
O cidadão usa literais de modelo em seu mecanismo de modelo padrão. Você pode instalar consolidar, atualizar a configuração do modelo e modificar os modelos de exibição padrão de acordo.
Para opções de configuração, consulte Configuração. Para mais concessionárias para ajudá -lo a começar, consulte Utilitários.
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 cidadão e iniciar seu aplicativo:
// start.js
import citizen from 'citizen'
global . app = citizen
app . start ( )Corra do terminal:
$ node start.jsVocê pode configurar seu aplicativo cidadão com um arquivo de configuração, opções de inicialização e/ou configurações de controlador personalizado.
O diretório de configuração é opcional e contém arquivos de configuração no formato JSON que impulsionam o Citizen e seu aplicativo. Você pode ter vários arquivos de configuração de cidadãos neste diretório, permitindo diferentes configurações com base no ambiente. O cidadão constrói sua configuração com base na seguinte hierarquia:
host que corresponda ao nome do host da máquina e, se encontrar um, estende a configuração padrão com a configuração do arquivo.host correspondente, ele procurará um arquivo chamado Citizen.json e cargas dessa configuração.Digamos que você queira administrar o Citizen no porto 8080 no ambiente de desenvolvimento local e você terá um banco de dados local ao qual seu aplicativo se conectará. Você pode criar um arquivo de configuração chamado local.json (ou dev.json, o que quiser) com o seguinte:
{
"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
}
}Essa configuração estenderia a configuração padrão apenas ao executar em sua máquina local. Usando esse método, você pode comprometer vários arquivos de configuração de diferentes ambientes com o mesmo repositório.
As configurações do banco de dados seriam acessíveis em qualquer lugar do seu aplicativo via app.config.db . Os nós citizen e host são reservados para a estrutura; Crie seus próprios nó (s) para armazenar suas configurações personalizadas.
Você pode definir a configuração do seu aplicativo na inicialização através app.start() . Se houver um arquivo de configuração, a configuração de inicialização estenderá o arquivo de configuração. Se não houver arquivo de configuração, a configuração de inicialização estende a configuração padrão do cidadão.
// Start an HTTPS server with a PFX file
app . start ( {
citizen : {
http : {
enabled : false
} ,
https : {
enabled : true ,
pfx : '/absolute/path/to/site.pfx'
}
}
} ) Para definir configurações personalizadas no nível do controlador de rota, exporte um objeto config (mais nos controladores de rota e ações na seção Controladores de rota).
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
}
}
} A seguir, representa a configuração padrão do cidadão, que é estendido por sua configuração:
{
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
}
}
} Aqui está um resumo completo das configurações do cidadão e do que eles fazem.
Ao iniciar um servidor, além das opções de configuração http e https do Citizen, você pode fornecer as mesmas opções que http.createServer () e https.CreateServer ().
A única diferença é como você passa os arquivos de chave. Como você pode ver nos exemplos acima, você passa pelo cidadão os caminhos dos arquivos para seus arquivos de chave. O cidadão lê os arquivos para você.
| Contexto | Tipo | Valor padrão | Descrição |
|---|---|---|---|
host | Corda | '' | Para carregar diferentes arquivos de configuração em diferentes ambientes, o cidadão conta com o nome do host do servidor como uma chave. Na startup, se o Citizen encontrar um arquivo de configuração com uma chave host que corresponda ao nome do host do servidor, ele escolhe esse arquivo de configuração. Isso não deve ser confundido com o hostname do servidor HTTP (veja abaixo). |
| cidadão | |||
mode | Corda | Verifica NODE_ENV primeiro, caso contrário, production | O modo de aplicação determina certos comportamentos de tempo de execução. Valores possíveis são os logs do console de modo de production e development de produção. O modo de desenvolvimento permite logs de console detalhado, opções de depuração de URL e substituição do módulo quente. |
global | Corda | app | A convenção para inicializar o cidadão no arquivo inicial atribui a estrutura a uma variável global. O padrão, que você verá referenciado ao longo da documentação, é app . Você pode alterar essa configuração se quiser usar outro nome. |
contentTypes | Variedade | [ 'text/html', 'text/plain', 'application/json', 'application/javascript' ] | Uma lista de permissões dos formatos de resposta para cada solicitação, com base no cabeçalho da solicitação Accept do cliente. Ao configurar os formatos disponíveis para controladores ou ações de rota individuais, toda a variedade de formatos disponíveis deve ser fornecida. |
errors | Corda | capture | Quando seu aplicativo lança um erro, o comportamento padrão é para o cidadão tentar se recuperar do erro e manter o aplicativo em execução. Definir esta opção para exit diz ao Citizen para registrar o erro e sair do processo. |
templateEngine | Corda | templateLiterals | O cidadão usa [Modelo Literal] (https://developer.mozilla.org/en-us/docs/web/javascript/reference/template_literals) Sintaxe para exibir a renderização por padrão. Opcionalmente, você pode instalar consolidar e usar qualquer mecanismo que ele suporta (por exemplo, instale o guidão e defina templateEngine para handlebars ). |
urlPath | Corda | / | Indica o caminho da URL que leva ao seu aplicativo. Se você deseja que seu aplicativo seja acessível via http://yoursite.com/my/app e não está usando outro servidor como um front -end para proxy a solicitação, essa configuração deve ser /my/app (não esqueça a barra principal). Essa configuração é necessária para que o roteador funcione. |
| http | |||
enabled | Booleano | true | Ativa o servidor HTTP. |
hostname | Corda | 127.0.0.1 | O nome do host no qual seu aplicativo pode ser acessado via HTTP. Você pode especificar uma string vazia para aceitar solicitações em qualquer nome de host. |
port | Número | 3000 | O número da porta em que o servidor HTTP do cidadão ouve solicitações. |
| https | |||
enabled | Booleano | false | Ativa o servidor HTTPS. |
hostname | Corda | 127.0.0.1 | O nome do host no qual seu aplicativo pode ser acessado via HTTPS. O padrão é localhost, mas você pode especificar uma string vazia para aceitar solicitações em qualquer nome de host. |
port | Número | 443 | O número da porta em que o servidor HTTPS do cidadão ouve solicitações. |
secureCookies | Booleano | true | Por padrão, todos os cookies definidos em uma solicitação HTTPS estão seguros. Defina esta opção como false para substituir esse comportamento, tornando todos os cookies inseguros e exigindo que você defina manualmente a opção secure na diretiva de cookies. |
connectionQueue | Inteiro | null | O número máximo de solicitações recebidas para fila. Se deixado não especificado, o sistema operacional determina o limite da fila. |
| Sessões | |||
enabled | Booleano | false | Ativa o escopo da sessão do usuário, que atribui a cada visitante um ID exclusivo e permite armazenar dados associados a esse ID no servidor de aplicativos. |
lifespan | Número inteiro positivo | 20 | Se as sessões estiverem ativadas, esse número representa a duração da sessão de um usuário, em minutos. As sessões expiraram automaticamente se um usuário estiver inativo por esse período de tempo. |
| layout | |||
controller | Corda | '' | Se você usar um controlador de layout global, poderá especificar o nome desse controlador aqui, em vez de usar a next diretiva em todos os seus controladores. |
view | Corda | '' | Por padrão, o controlador de layout usará a visualização de layout padrão, mas você pode especificar uma visualização diferente aqui. Use o nome do arquivo sem a extensão do arquivo. |
| formas | |||
enabled | Booleano | true | O cidadão fornece processamento básico de carga útil para formas simples. Se você preferir usar um pacote de formulário separado, defina -o como false . |
maxPayloadSize | Número inteiro positivo | 524288 | Tamanho máximo da carga de pagamento, em bytes. Defina um tamanho máximo da carga útil para impedir que seu servidor seja sobrecarregado por dados de entrada de formulário. |
| compressão | |||
enabled | Booleano | false | Ativa o GZIP e deflate a compactação para visões renderizadas e ativos estáticos. |
force | Booleano ou string | false | Força o GZIP ou a codificação para todos os clientes, mesmo que eles não relatem aceitar formatos compactados. Muitos proxies e firewalls quebram o cabeçalho de codificação de aceitação que determina o suporte do GZIP e, como todos os clientes modernos suportam o GZIP, geralmente é seguro forçá-lo definindo isso como gzip , mas você também pode forçar deflate . |
mimeTypes | Variedade | Veja a configuração padrão acima. | Uma matriz de tipos de MIME que serão compactados se a compactação estiver ativada. Consulte a configuração de amostra acima para a lista padrão. Se você deseja adicionar ou remover itens, substitua a matriz na íntegra. |
| cache | |||
control | Objeto contendo pares de chave/valor | {} | Use essa configuração para definir cabeçalhos de controle de cache para controladores de rota e ativos estáticos. A chave é o nome do caminho do ativo e o valor é o cabeçalho do controle de cache. Consulte o cache do lado do cliente para obter detalhes. |
invalidUrlParams | Corda | warn | A opção de cache de rota pode especificar parâmetros de URL válidos para impedir que os URLs ruins sejam armazenados em cache, e invalidUrlParams determina se deve registrar um aviso ao encontrar URLs ruins ou lançar um erro no lado do cliente. Consulte solicitações de cache e ações do controlador para obter detalhes. |
| Cache.Application | |||
enabled | Booleano | true | Ativa o cache na memória, acessado através dos métodos cache.set() e cache.get() . |
lifespan | Número | 15 | O período de tempo em que um ativo de aplicação em cache permanece na memória, em minutos. |
resetOnAccess | Booleano | true | Determina se reinicia o temporizador do cache em um ativo em cache sempre que o cache for acessado. Quando definido como false , os itens em cache expira quando a lifespan é alcançada. |
encoding | Corda | utf-8 | Quando você passa um caminho de arquivo para cache.set (), a configuração de codificação determina o que a codificação deve ser usada ao ler o arquivo. |
synchronous | Booleano | false | Quando você passa um caminho de arquivo para cache.set (), essa configuração determina se o arquivo deve ser lido de maneira síncrona ou assíncrona. Por padrão, as leituras de arquivo são assíncronas. |
| cache.static | |||
enabled | Booleano | false | Ao servir arquivos estáticos, o cidadão normalmente lê o arquivo do disco para cada solicitação. Você pode acelerar o arquivo estático que serve consideravelmente, definindo isso como true , que armazenam em cache buffers de arquivo na memória. |
lifespan | Número | 15 | O período de tempo que um ativo estático em cache permanece na memória, em minutos. |
resetOnAccess | Booleano | true | Determina se a redefinição do cache em um ativo estático em cache sempre que o cache for acessado. Quando definido como false , os itens em cache expira quando a lifespan é alcançada. |
| logs | |||
access | Booleano | false | Ativa os arquivos de log de acesso HTTP. Desativado por padrão porque os logs de acesso podem explodir de maneira rápida e ideal, ele deve ser tratado por um servidor da Web. |
debug | Booleano | false | Ativa os arquivos de log de depuração. Útil para depurar problemas de produção, mas extremamente detalhado (os mesmos logs que você veria no console no modo de desenvolvimento). |
maxFileSize | Número | 10000 | Determina o tamanho máximo do arquivo dos arquivos de log, em Kilobytes. Quando o limite é atingido, o arquivo de log é renomeado com um carimbo de hora e um novo arquivo de log é criado. |
| logs.error | |||
client | Booleano | true | Permite o registro de erros de cliente de 400 níveis. |
server | Booleano | false | Ativa o registro de erros de servidor/aplicativo de 500 níveis. |
status | Booleano | false | Controla se as mensagens de status devem ser registradas no console quando estiver no modo de produção. (O modo de desenvolvimento sempre registra o console.) |
| logs.watcher | |||
interval | Número | 60000 | Para sistemas operacionais que não suportam eventos de arquivo, esse cronômetro determina com que frequência os arquivos de log serão pesquisados para alterações antes do arquivamento, em milissegundos. |
| desenvolvimento | |||
| Development.debug | |||
scope | Objeto | Essa configuração determina quais escopos estão registrados na saída de depuração no modo de desenvolvimento. Por padrão, todos os escopos estão ativados. | |
depth | Número inteiro positivo | 3 | Quando o cidadão despeja um objeto no conteúdo de depuração, ele o inspeciona usando o uso do Node. Essa configuração determina a profundidade da inspeção, o que significa o número de nós que serão inspecionados e exibidos. Números maiores significam inspeção mais profunda e desempenho mais lento. |
view | Booleano | false | Defina isso como true para despejar informações de depuração diretamente na visualização HTML. |
enableCache | Booleano | false | Modo de desenvolvimento desativa o cache. Altere essa configuração para true para ativar o cache no modo de desenvolvimento. |
| Development.watcher | |||
custom | Variedade | Você pode dizer à substituição do módulo quente do cidadão para assistir seus próprios módulos personalizados. Essa matriz pode conter objetos com watch (caminho do diretório relativo para seus módulos no diretório do aplicativo) e assign (a variável para a qual você atribui esses módulos) propriedades. Exemplo:[ { "watch": "/util", "assign": "app.util" } ] | |
O Citizen usa a Chokidar como seu observador de arquivos, portanto, a opção watcher para os logs e o modo de desenvolvimento também aceita qualquer opção permitida por Chokidar.
Essas configurações são expostas publicamente via app.config.host e app.config.citizen .
Esta documentação assume que seu nome de variável de aplicativo global é app . Ajustar de acordo.
app.start() | Inicia um servidor de aplicativos da Web Citizen. |
app.config | As configurações de configuração fornecidas na inicialização. As configurações do cidadão estão em app.config.citizen . |
app.controllersapp.modelsapp.views | É improvável que você precise acessar controladores e visualizações diretamente, mas referenciando app.models em vez de importar seus modelos se beneficiar manualmente da substituição do módulo quente interno do cidadão. |
app.helpers | Todos os módulos de auxiliar/utilidade colocados em app/helpers/ são importados para o objeto Helpers. |
app.cache.set()app.cache.get()app.cache.exists()app.cache.clear() | Cache de aplicativos e armazenamento de chave/valor usado internamente pelo cidadão, também disponível para o seu aplicativo. |
app.log() | Console básico e registro de arquivos usados pelo cidadão, exportado para seu uso. |
A estrutura do URL do cidadão determina qual controlador de rota e ação para disparar, passa os parâmetros da URL e abre um pouco de espaço para conteúdo amigável ao SEO que pode dobrar como um identificador exclusivo. A estrutura se parece com a seguinte:
http://www.site.com/controller/seo-content/action/myAction/param/val/param2/val2
Por exemplo, digamos que o URL base do seu site seja:
http://www.cleverna.me
O controlador de rota padrão é index e a ação padrão é handler() , então o acima é o equivalente aos seguintes:
http://www.cleverna.me/index/action/handler
Se você tem um controlador de rota article , solicitaria assim:
http://www.cleverna.me/article
Em vez de seqüências de consultas, o cidadão passa os parâmetros de URL que consistem em pares de nome/valor. Se você tivesse que passar por um artigo de 237 e um número de página de 2, anexaria os pares de nome/valor ao URL:
http://www.cleverna.me/article/id/237/page/2
Os nomes de parâmetros válidos podem conter letras, números, sublinhados e traços, mas devem começar com uma letra ou sublinhamento.
A ação do controlador padrão é handler() , mas você pode especificar ações alternativas com o parâmetro action (mais sobre isso mais tarde):
http://www.cleverna.me/article/action/edit
O cidadão também permite que você insira opcionalmente conteúdo relevante em seus URLs, assim:
http://www.cleverna.me/article/My-Clever-Article-Title/page/2
Esse conteúdo de SEO deve sempre seguir o nome do controlador e preceder qualquer nome de nome/valor, incluindo a ação do controlador. Você pode acessá -lo genericamente via route.descriptor ou dentro do escopo url ( url.article neste caso), o que significa que você pode usá -lo como um identificador exclusivo (mais nos parâmetros URL na seção Controladores de rota).
A action e a direct dos parâmetros da URL são reservados para a estrutura; portanto, não os use para o seu aplicativo.
O cidadão conta com uma simples convenção de controlador de vista modelo. O padrão do artigo mencionado acima pode usar a seguinte estrutura:
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
Pelo menos um controlador de rota é necessário para um determinado URL e o arquivo de visualização padrão de um controlador de rota deve compartilhar seu nome. Os modelos são opcionais.
Todas as visualizações para um determinado controlador de rota podem existir no app/views/ diretório, ou podem ser colocados em um diretório cujo nome corresponde ao do controlador para organização mais limpa:
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html // The default view
edit.html // Alternate article views
delete.html
Mais sobre as visualizações na seção Visualizações.
Modelos e visualizações são opcionais e não precisam necessariamente estar associados a um controlador específico. Se o seu controlador de rota passará sua saída para outro controlador para processamento adicional e renderização final, você não precisará incluir uma visualização correspondente (consulte a diretiva da Next Controller).
Um controlador de rota cidadão é apenas um módulo JavaScript. Cada controlador de rota requer pelo menos uma exportação para servir de ação para a rota solicitada. A ação padrão deve ser nomeada handler() , que é chamada pelo Citizen quando nenhuma ação é especificada no URL.
// Default route controller action
export const handler = async ( params , request , response , context ) => {
// Do some stuff
return {
// Send content and directives to the server
}
} O Citizen Server chama handler() depois de processar a solicitação inicial e passa por 4 argumentos: um objeto params que contém os parâmetros da solicitação, os objetos request e response Node.js e o contexto da solicitação atual.
params config | A configuração do seu aplicativo, incluindo quaisquer personalizações para a ação atual do controlador |
route | Detalhes da rota solicitada, como o URL e o nome do controlador de rota |
url | Quaisquer parâmetros derivados do URL |
form | Dados coletados de uma postagem |
payload | A carga útil de solicitação bruta |
cookie | Cookies enviados com o pedido |
session | Variáveis de sessão, se as sessões estiverem ativadas |
Além de ter acesso a esses objetos dentro do seu controlador, eles também são incluídos no seu contexto de visualização automaticamente, para que você possa referenciá -los em seus modelos de visualização como variáveis locais (mais detalhes na seção de visualizações).
Por exemplo, com base no artigo anterior do artigo ...
http://www.cleverna.me/article/My-Clever-Article-Title/id/237/page/2
... você terá o seguinte objeto params.url passado ao seu controlador:
{
article : 'My-Clever-Article-Title' ,
id : '237' ,
page : '2'
} O nome do controlador se torna uma propriedade no escopo da URL que faz referência ao descritor, o que o torna adequado para uso como um identificador exclusivo. Também está disponível no objeto params.route como params.route.descriptor .
O argumento context contém quaisquer dados ou diretrizes que foram geradas por controladores anteriores na cadeia usando sua declaração return .
Para retornar os resultados da ação do controlador, inclua uma declaração return com quaisquer dados e diretrizes que você deseja passar para o cidadão.
Usando os parâmetros de URL acima, posso recuperar o conteúdo do artigo do modelo e passar de volta ao 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
}
}
} Ações alternativas podem ser solicitadas usando o parâmetro de URL action . Por exemplo, talvez queremos uma ação e visão diferentes para editar um artigo:
// 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'
}
} Você coloca todos os dados que desejar de volta ao cidadão dentro da declaração return . Todos os dados que você deseja renderizar em sua opinião devem ser transmitidos ao cidadão dentro de um objeto chamado local , como mostrado acima. Objetos adicionais podem ser transmitidos ao Citizen para definir diretrizes que fornecem instruções ao servidor (consulte as Diretivas do Controlador). Você pode até adicionar seus próprios objetos ao contexto e passá -los do controlador para o controlador (mais na seção de encadeamento do controlador.)
Os modelos são módulos opcionais e sua estrutura depende completamente de você. O cidadão não fala diretamente com seus modelos; Ele os armazena apenas em app.models para sua conveniência. Você também pode importá -los manualmente para seus controladores, se preferir.
A função a seguir, quando colocada em app/models/article.js , estará acessível em seu aplicativo via 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
} O cidadão usa literais de modelo para exibir a renderização por padrão. Você pode instalar o consolidate.js e usar qualquer mecanismo de modelo suportado. Basta atualizar a configuração do templateEngine de acordo.
No article.html , você pode fazer referência a variáveis que você colocou dentro do objeto local passado para a declaração de retorno do controlador de rota. O cidadão também injeta propriedades do objeto params em seu contexto de visualização automaticamente, para que você tenha acesso a esses objetos como variáveis locais (como o escopo 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 padrão, o servidor renderiza a visualização cujo nome corresponde ao do controlador. Para renderizar uma visão diferente, use a diretiva de view na sua declaração de retorno.
Todas as visualizações entram em /app/views . Se um controlador tiver várias visualizações, você poderá organizá -las em um diretório com o nome desse controlador.
app/
controllers/
routes/
article.js
index.js
views/
article/
article.html // Default article controller view
edit.html
index.html // Default index controller view
Você pode dizer a um controlador de rota para retornar suas variáveis locais como JSON ou JSON-P, definindo o cabeçalho Accept apropriado em sua solicitação, permitindo que o mesmo recurso sirva uma visualização HTML completa e JSON para solicitações AJAX e APIs RESTful.
A ação do handler() de rota do artigo retornaria:
{
"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] "
}
} O que você adicionou à declaração de retorno do controlador, o objeto local será retornado.
Para JSONP, use callback no URL:
http://www.cleverna.me/article/My-Clever-Article-Title/callback/foo
Retornos:
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 forçar um tipo de conteúdo específico para uma determinada solicitação, defina response.contentType no controlador de rota para a saída desejada:
export const handler = async ( params , request , response ) => {
// Every request will receive a JSON response regardless of the Accept header
response . contentType = 'application/json'
}Você pode forçar um tipo de resposta global em todas as solicitações dentro de um gancho de evento.
Os ajudantes são módulos de utilidade opcionais e sua estrutura depende completamente de você. Eles são armazenados no app.helpers para sua conveniência. Você também pode importá -los manualmente para seus controladores e modelos, se preferir.
A função a seguir, quando colocada em app/helpers/validate.js , estará acessível em seu aplicativo via 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 )
} O Citizen armazena todos os módulos no escopo do app , não apenas para recuperar fácil, mas para apoiar a substituição do módulo quente (HMR). Quando você economiza alterações em qualquer módulo ou visualização no modo de desenvolvimento, o cidadão limpa a importação existente do módulo e reimporta esse módulo em tempo real.
Você verá um log de console observando o arquivo afetado e seu aplicativo continuará sendo executado. Não há necessidade de reiniciar.
O cidadão faz o possível para lidar com erros graciosamente sem sair do processo. A ação seguinte do controlador apresentará um erro, mas o servidor responderá com um 500 e continuará em execução:
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
}
}Você também pode lançar um erro manualmente e personalizar a mensagem de erro:
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
}
} Observe que params.route.controller é atualizado do controlador solicitado como error , portanto, qualquer referência em seu aplicativo para o controlador solicitado deve levar isso em consideração.
Os erros são retornados no formato solicitado pela rota. Se você solicitar JSON e a rota lançar um erro, o cidadão retornará o erro no formato JSON.
O esqueleto do aplicativo criado pelo utilitário de andaime inclui modelos opcionais de exibição de erro para erros comuns de cliente e servidor, mas você pode criar modelos para qualquer código de erro HTTP.
O método de manuseio de erro padrão do cidadão é capture , que tenta uma recuperação graciosa. Se você preferir sair do processo após um erro, altere config.citizen.errors para exit .
// config file: exit the process after an error
{
"citizen" : {
"errors" : "exit"
}
}Após o manipulador de erros do aplicativo, o cidadão sairá do processo.
Para criar visualizações de erros personalizadas para erros do servidor, crie um diretório chamado /app/views/error e preencha -o com modelos nomeados após o código de resposta HTTP ou o código de erro do nó.
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
Além dos dados de visualização, a instrução de retorno do controlador de rota também pode passar as diretrizes para renderizar visualizações alternativas, definir cookies e variáveis de sessão, iniciar redirecionamentos, ligar e renderizar, inclui ações/visualizações do controlador de rota de cache (ou solicitações inteiras) e entregar a solicitação a outro controlador para processamento posterior.
Por padrão, o servidor renderiza a visualização cujo nome corresponde ao do controlador. Para renderizar uma visão diferente, use a Diretiva view em sua declaração de devolução:
// 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'
}
} Você define cookies retornando um objeto de cookie dentro da ação do 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
}
}
}
}Aqui está um exemplo de configurações padrão de um 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
} Depois que os cookies são definidos no cliente, eles estão disponíveis em params.cookie dentro dos controladores e simplesmente cookie dentro da exibição:
<!doctype html >
< html >
< body >
< section >
Welcome, ${cookie.username}.
</ section >
</ body >
</ html > As variáveis de cookie que você definiu dentro do seu controlador não está disponível imediatamente no escopo params.cookie . O cidadão precisa receber o contexto do controlador e enviar a resposta primeiro ao cliente; portanto, use uma instância local da variável se precisar acessá -la durante a mesma solicitação.
Todos os cookies definidos pelo Citizen começam com o prefixo ctzn_ para evitar colisões. Não inicie seus nomes de biscoitos com ctzn_ e você não terá problemas.
Se você usa o Citizen por trás de um proxy, como Nginx ou Apache, verifique se você tem um cabeçalho Forwarded HTTP na configuração do servidor, para que o manuseio de cookies seguros do Citizen funcione corretamente.
Aqui está um exemplo de como você pode configurar isso no nginx:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:8080;
}
Se as sessões estiverem ativadas, você poderá acessar variáveis de sessão via params.session no seu controlador ou simplesmente session dentro das visualizações. Esses escopos locais fazem referência à sessão do usuário atual sem precisar passar por um ID da sessão.
Por padrão, uma sessão possui quatro propriedades: id , started , expires e timer . O ID da sessão também é enviado ao cliente como um cookie chamado ctzn_session_id .
Definir variáveis de sessão é praticamente a mesma que definir variáveis de cookies:
return {
session : {
username : 'Danny' ,
nickname : 'Doc'
}
} Como os cookies, as variáveis de sessão que você acabou de atribuir não estão disponíveis durante a mesma solicitação no escopo params.session , portanto, use uma instância local se precisar acessar esses dados imediatamente.
As sessões expiram com base nas sessions.lifespan . O padrão é de 20 minutos. O timer é redefinido com cada solicitação do usuário. Quando o timer acaba, a sessão é excluída. Quaisquer solicitações do cliente após esse tempo gerarão um novo ID da sessão e enviarão um novo cookie de ID da sessão para o cliente.
Para limpar à força e expirar a sessão do usuário atual:
return {
session : {
expires : 'now'
}
} Todas as variáveis de sessão definidas pelo cidadão começam com o prefixo ctzn_ para evitar colisões. Não inicie seus nomes de variáveis de sessão com ctzn_ e você não terá problemas.
Você pode passar as instruções de redirecionamento para o servidor que serão iniciadas após o processamento da ação do controlador.
O objeto redirect pega uma sequência de URL em sua versão abreviada, ou três opções: statusCode , url e refresh . Se você não fornecer um código de status, o Citizen usa 302 (redirecionamento temporário). A opção refresh determina se o redirecionamento usa um cabeçalho de localização ou o cabeçalho de atualização fora do padrão.
// 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
}
} Ao contrário do cabeçalho do local, se você usar a opção refresh , o cidadão enviará uma visão renderizada ao cliente porque o redirecionamento ocorre no lado do cliente.
Usando as quebras do cabeçalho do local (na minha opinião) o cabeçalho do referente, porque o referente acaba não sendo o recurso que iniciou o redirecionamento, mas o recurso anterior à página que o iniciou. Para contornar esse problema, o Citizen armazena uma variável de sessão chamada ctzn_referer que contém o URL do recurso que iniciou o redirecionamento, que você pode usar para redirecionar os usuários corretamente. Por exemplo, se um usuário não autenticado tentar acessar uma página segura e você redirecioná -lo para um formulário de login, o endereço da página segura será armazenado no ctzn_referer para que você possa enviá -los para lá em vez da página anterior.
Se você não habilitou as sessões, o Citizen recorre à criação de um cookie chamado ctzn_referer .
Se você usa o Citizen por trás de um proxy, como Nginx ou Apache, verifique se você tem um cabeçalho Forwarded HTTP na configuração do servidor, para que ctzn_referer funcione corretamente.
Aqui está um exemplo de como você pode configurar isso no nginx:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:8080;
}
Você pode definir cabeçalhos HTTP usando a diretiva header :
return {
header : {
'Cache-Control' : 'max-age=86400' ,
'Date' : new Date ( ) . toISOString ( )
}
} Você também pode definir cabeçalhos diretamente usando o método response.setHeader() do Node, mas o uso da diretiva header do Citizen preserva esses cabeçalhos no cache da solicitação, para que eles sejam aplicados sempre que essa ação do controlador for puxada do cache.
O cidadão permite usar padrões completos de MVC, conforme inclui, que são a versão dos componentes do cidadão. Cada um tem seu próprio controlador de rota, modelo e visualização (s). Inclui pode ser usado para executar uma ação ou retornar uma visualização completa renderizada. Qualquer controlador de rota pode ser uma inclusão.
Digamos que o modelo do padrão do artigo tenha o seguinte conteúdo. A seção da cabeça contém meta dados dinâmicos, e o conteúdo do cabeçalho muda, dependendo se o usuário está conectado ou não:
<!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 >Provavelmente faz sentido usar inclui a seção e o cabeçalho da cabeça, porque você usará esse código em todos os lugares, mas, em vez de parciais simples, você pode criar o cidadão. A seção principal pode usar seu próprio modelo para preencher os meta -dados e, como o cabeçalho é diferente para usuários autenticados, vamos retirar essa lógica da vista e colocá -la no controlador do cabeçalho. Eu gosto de seguir a convenção de iniciar parciais com um sublinhado, mas isso depende de você:
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
Quando o controlador do artigo é demitido, ele deve dizer ao cidadão, o que inclui as necessidades. Fazemos isso com a diretiva 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'
}
}
}
} Os cidadãos incluem padrões têm os mesmos requisitos que os padrões regulares, incluindo um controlador com uma ação pública. A diretiva include acima diz ao cidadão que ligue para os controladores _head e _header , passe os mesmos argumentos que foram passados para o controlador article (params, solicitação, resposta, contexto), renderizam suas respectivas visões e adicionam as visões resultantes ao contexto de visualização.
Aqui está a aparência do nosso controlador de seção principal:
// _head controller
export const article = async ( params ) => {
let metaData = await app . models . _head ( { article : params . url . article } )
return {
local : {
metaData : metaData
}
}
}E a visualização da seção da cabeça:
< 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 >Aqui está a aparência do nosso controlador de cabeçalho:
// _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'
}
}E o cabeçalho visualiza:
<!-- /views/_header/_header.html -->
< header >
< a href =" /login " > Login </ a >
</ header > <!-- /views/_header/_header-authenticated.html -->
< header >
< p > Welcome, ${cookie.username} </ p >
</ header > Os inclui inclui são armazenados no escopo 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 >O cidadão inclui são independentes e entregues ao controlador de chamada como uma visão totalmente renderizada. Enquanto eles recebem os mesmos dados (parâmetros de URL, entradas de formação, contexto de solicitação etc.) que o controlador de chamada, os dados gerados dentro de uma inclusão não são transmitidos de volta ao chamador.
Um padrão destinado a ser usado como inclusão pode ser acessado via HTTP como qualquer outro controlador de rota. Você pode solicitar o controlador _header como assim e receber um pedaço de HTML ou JSON como uma resposta:
http://cleverna.me/_header
Isso é ótimo para lidar com a primeira solicitação do lado do servidor e atualizar o conteúdo com uma biblioteca do lado do cliente.
O cidadão inclui fornecer funcionalidade rica, mas eles têm limitações e podem ser exagerados em determinadas situações.
O cidadão permite que você acorrente vários controladores de rota em série de uma única solicitação usando a next diretiva. O controlador solicitado passa seus dados e renderizou a visão para um controlador subsequente, adicionando seus próprios dados e renderizando sua própria visão.
Você pode amarrar quantos controladores de rota juntos em uma única solicitação quiser. Cada controlador de rota terá seus dados e visualização de saída armazenada no 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 da cadeia tem acesso ao contexto e das visualizações do controlador anterior. O último controlador da cadeia fornece a visão final renderizada. A layout controller with all your site's global elements is a common use for this.
// 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.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 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.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.