Citizen是一个基于MVC的Web应用程序框架,旨在有兴趣快速构建快速,可扩展的网站的人,而不是在Node的胆量上挖掘或将摇摆不定的Jenga Tower拼凑在一起,由50个不同的包装制成。
使用Citizen作为传统服务器端网络应用程序,模块化单页应用程序(SPA)或RESTFUL API的基础。
从0.9.x到1.0.x的过渡发生了许多破裂的变化。请咨询ChangElog以获取逐项列表,并彻底查看此更新的文档。
显然,这比任何NPM/github readme所包含的内容都要多。我正在为该文档的网站工作。
我在个人网站和OriginalTrilogy.com上使用公民。 OT.com在一个$ 30的云托管计划上处理一个公民实例的$ 30云托管计划,处理中等数量的流量(每月几十万次观看次数),该应用程序/过程一次每次运行几个月而不会崩溃。这很稳定。
这些命令将为您的Web应用程序创建一个新目录,安装Citizen,使用其脚手架实用程序来创建应用程序的骨架,然后启动Web服务器:
$ mkdir myapp && cd myapp
$ npm install citizen
$ node node_modules/citizen/util/scaffold skeleton
$ node app/start.js如果一切顺利,您将在控制台中看到Web服务器正在运行的确认。访问浏览器中的http://127.0.0.1:3000,您会看到一个裸露的索引模板。
公民在其默认模板引擎中使用模板文字。您可以安装合并,更新模板配置,并相应地修改默认视图模板。
有关配置选项,请参见配置。有关更多公用事业,可以帮助您开始使用,请参阅公用事业。
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
进口公民并启动您的应用程序:
// start.js
import citizen from 'citizen'
global . app = citizen
app . start ( )从终端运行:
$ node start.js您可以使用配置文件,启动选项和/或自定义控制器配置配置公民应用程序。
配置目录是可选的,并且包含以JSON格式的配置文件,该文件同时驱动公民和您的应用程序。您可以在此目录中拥有多个公民配置文件,从而允许基于环境的不同配置。公民根据以下层次结构建立其配置:
host密钥,如果找到一个键,则使用文件配置扩展了默认配置。host密钥,它将寻找一个名为Citizen.json的文件并加载该配置。假设您想在本地开发环境中在端口8080上运行公民,并且您的应用程序将连接到本地数据库。您可以使用以下内容创建一个名为local.json(或Dev.json,无论您想要的)的配置文件:
{
"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
}
}此配置仅在本地计算机上运行时仅扩展默认配置。使用此方法,您可以将多个配置文件从不同的环境提交到同一存储库。
数据库设置将通过app.config.db在您的应用程序中的任何地方访问。 citizen host保留用于框架;创建自己的节点以存储您的自定义设置。
您可以通过app.start()在启动时设置应用程序的配置。如果有配置文件,启动配置将扩展配置文件。如果没有配置文件,则启动配置扩展了默认的公民配置。
// Start an HTTPS server with a PFX file
app . start ( {
citizen : {
http : {
enabled : false
} ,
https : {
enabled : true ,
pfx : '/absolute/path/to/site.pfx'
}
}
} ) 要在路由控制器级别设置自定义配置,请导出一个config对象(更多关于路由控制器和“路由控制器”部分中的操作)。
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
}
}
} 以下表示公民的默认配置,该配置是通过您的配置扩展的:
{
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
}
}
} 这是对公民的设置及其所做的事情的完整摘要。
启动服务器时,除了公民的http和https配置选项外,还可以提供与Node的HTTP.Createserver()和https.createserver()相同的选项。
唯一的区别是您如何通过密钥文件。正如您在上面的示例中看到的那样,您将密钥文件的文件路径传递给公民。公民为您阅读文件。
| 环境 | 类型 | 默认值 | 描述 |
|---|---|---|---|
host | 细绳 | '' | 要在不同环境中加载不同的配置文件,公民依靠服务器的主机名作为键。在启动时,如果Citizen找到了与服务器主机名匹配的host密钥的配置文件,它将选择该配置文件。这不是与HTTP服务器hostname混淆(请参见下文)。 |
| 公民 | |||
mode | 细绳 | 首先检查NODE_ENV ,否则production | 应用模式确定某些运行时行为。可能的值是production和development生产模式沉默控制台日志。开发模式启用详细的控制台日志,URL调试选项和热模块更换。 |
global | 细绳 | app | 在开始文件中初始化公民的公约将框架分配给全局变量。您将在整个文档中看到的默认值是app 。如果要使用另一个名称,则可以更改此设置。 |
contentTypes | 大批 | [ 'text/html', 'text/plain', 'application/json', 'application/javascript' ] | 根据客户的Accept请求标题,每个请求的响应格式允许列表。在为单个路由控制器或操作配置可用格式时,必须提供整个可用格式的数组。 |
errors | 细绳 | capture | 当您的应用程序引发错误时,默认行为是让公民尝试从错误中恢复并保持应用程序运行的行为。设置此选项exit ,告诉公民记录错误并退出该过程。 |
templateEngine | 细绳 | templateLiterals | 公民使用[模板文字](https://ddeveloper.mozilla.org/en-us/docs/web/javascript/reference/reference/template_literals)语法用于默认情况下的视图呈现。可选地,您可以安装合并并使用其支持的任何引擎(例如,安装柄栏并将templateEngine设置为handlebars )。 |
urlPath | 细绳 | / | 表示通往您应用的URL路径。如果您希望通过http://yoursite.com/my/app访问您的应用程序,并且您不会使用另一台服务器作为代理请求的前端,则此设置应为/my/app (不要忘记领先的Slash)。路由器工作需要此设置。 |
| http | |||
enabled | 布尔 | true | 启用HTTP服务器。 |
hostname | 细绳 | 127.0.0.1 | 可以通过HTTP访问应用程序的主机名。您可以指定一个空字符串以在任何主机名中接受请求。 |
port | 数字 | 3000 | 公民HTTP服务器上的端口号聆听请求。 |
| https | |||
enabled | 布尔 | false | 启用HTTPS服务器。 |
hostname | 细绳 | 127.0.0.1 | 可以通过HTTPS访问应用程序的主机名。默认值是本地主机,但是您可以指定一个空字符串以在任何主机名中接受请求。 |
port | 数字 | 443 | 公民HTTPS服务器上的端口号聆听请求。 |
secureCookies | 布尔 | true | 默认情况下,HTTPS请求中设置的所有cookie都是安全的。将此选项设置为false以覆盖该行为,使所有Cookie都不安全,并要求您在Cookie指令中手动设置secure选项。 |
connectionQueue | 整数 | null | 排队的最大传入请求数量。如果未指定,操作系统将确定队列限制。 |
| 会议 | |||
enabled | 布尔 | false | 启用用户会话范围,该范围将每个访问者分配一个唯一的ID,并允许您将与该ID关联的数据存储在应用程序服务器中。 |
lifespan | 正整数 | 20 | 如果启用了会话,则此数字表示用户会话的长度,以分钟为单位。如果用户在此时间不活跃的情况下,会议会自动过期。 |
| 布局 | |||
controller | 细绳 | '' | 如果使用全局布局控制器,则可以在此处指定该控制器的名称,而不是在所有控制器中使用next指令。 |
view | 细绳 | '' | 默认情况下,布局控制器将使用默认布局视图,但是您可以在此处指定其他视图。使用没有文件扩展名的文件名。 |
| 表格 | |||
enabled | 布尔 | true | 公民为简单表格提供基本的有效负载处理。如果您希望使用单独的表单软件包,请将其设置为false 。 |
maxPayloadSize | 正整数 | 524288 | 最大表单有效载荷大小,字节。设置最大有效载荷大小,以防止您的服务器被表单输入数据超载。 |
| 压缩 | |||
enabled | 布尔 | false | 启用GZIP和DEFLATE压缩,以进行渲染的视图和静态资产。 |
force | 布尔或字符串 | false | 即使他们不报告接受压缩格式,也强制GZIP或Deflate编码。许多代理和防火墙都打破了确定GZIP支持的接受编码标头,并且由于所有现代客户都支持GZIP,因此通常可以通过将其设置为gzip来安全,但您也可以强迫deflate 。 |
mimeTypes | 大批 | 请参阅上面的默认配置。 | 如果启用压缩,将压缩的一系列MIME类型。有关默认列表,请参见上面的示例配置。如果要添加或删除项目,则必须完整替换数组。 |
| 缓存 | |||
control | 包含密钥/值对的对象 | {} | 使用此设置为路由控制器和静态资产设置高速缓存标头。关键是资产的路径名,值是高速缓存标头。有关详细信息,请参见客户端缓存。 |
invalidUrlParams | 细绳 | warn | 路由缓存选项可以指定有效的URL参数,以防止不良URL被缓存,并且invalidUrlParams确定在遇到不良URL或丢弃客户端错误时是否记录警告。有关详细信息,请参见缓存请求和控制器操作。 |
| cache.application | |||
enabled | 布尔 | true | 启用通过cache.set()和cache.get()方法访问的内存缓存。 |
lifespan | 数字 | 15 | 缓存的应用程序资产的时间长度在几分钟内保留在内存中。 |
resetOnAccess | 布尔 | true | 每当访问缓存时,确定是否在缓存资产上重置缓存计时器。设置为false时,缓存的物品到达lifespan时到期。 |
encoding | 细绳 | utf-8 | 当您将文件路径传递到CACHE.SET()时,编码设置确定读取文件时应使用哪些编码。 |
synchronous | 布尔 | false | 当您将文件路径传递到CACHE.SET()时,此设置确定是否应同步或异步读取文件。默认情况下,文件读取为异步。 |
| cache.static | |||
enabled | 布尔 | false | 服务静态文件时,公民通常会从磁盘上读取每个请求的文件。您可以通过将其设置为true来大大加快静态文件的速度,该文件将文件缓冲器缓存在内存中。 |
lifespan | 数字 | 15 | 缓存的静态资产的时间长度在几分钟内保留在记忆中。 |
resetOnAccess | 布尔 | true | 确定是否在访问缓存时是否在缓存的静态资产上重置缓存计时器。设置为false时,缓存的物品到达lifespan时到期。 |
| 日志 | |||
access | 布尔 | false | 启用HTTP访问日志文件。默认情况下禁用,因为访问日志可以快速爆炸,理想情况下应由Web服务器处理。 |
debug | 布尔 | false | 启用调试日志文件。可用于调试生产问题,但详细的内容(与开发模式下的控制台可以看到相同的日志)。 |
maxFileSize | 数字 | 10000 | 以千字节确定日志文件的最大文件大小。当达到限制时,日志文件将重命名为时间戳,并创建新的日志文件。 |
| logs.Error | |||
client | 布尔 | true | 启用400级客户端错误的记录。 |
server | 布尔 | false | 启用500级服务器/应用程序错误的记录。 |
status | 布尔 | false | 控制状态消息在生产模式下是否应登录到控制台。 (开发模式始终登录到控制台。) |
| logs.watcher | |||
interval | 数字 | 60000 | 对于不支持文件事件的操作系统,此计时器确定以毫秒之前的归档前进行更改的日志文件的频率。 |
| 发展 | |||
| development.debug | |||
scope | 目的 | 此设置确定在开发模式下的调试输出中记录了哪些范围。默认情况下,启用了所有范围。 | |
depth | 正整数 | 3 | 当公民将对象倾倒在调试内容中时,它会使用Node的Util.Insproct进行检查。此设置确定了检查的深度,这意味着将检查和显示的节点数量。较大的数字意味着更深的检查和较慢的性能。 |
view | 布尔 | false | 将其设置为true,将调试信息直接转储到HTML视图中。 |
enableCache | 布尔 | false | 开发模式禁用缓存。将此设置更改为true ,以在开发模式下启用缓存。 |
| Development.Watcher | |||
custom | 大批 | 您可以告诉公民的热模块更换以观看自己的自定义模块。此数组可以包含具有watch (应用程序目录中模块的相对目录路径)的对象,并assign (您分配这些模块的变量)属性。例子:[ { "watch": "/util", "assign": "app.util" } ] | |
Citizen使用Chokidar作为其文件观察者,因此对日志和开发模式的watcher选项也接受Chokidar允许的任何选项。
这些设置通过app.config.host和app.config.citizen公开公开。
该文档假设您的全局应用变量名称是app 。相应调整。
app.start() | 启动公民Web应用程序服务器。 |
app.config | 您在启动时提供的配置设置。公民的设置在app.config.citizen中。 |
app.controllersapp.modelsapp.views | 您不太可能需要直接访问控制器和视图,但是引用app.models而不是手动从公民内置的热模块更换中受益。 |
app.helpers | 将所有助手/实用程序模块放在app/helpers/助手中。 |
app.cache.set()app.cache.get()app.cache.exists()app.cache.clear() | 公民内部使用的应用程序缓存和密钥/值存储,也可用于您的应用。 |
app.log() | 公民使用的基本控制台和文件记录,用于您使用。 |
公民URL结构确定了要开火的路线控制器和动作,通过URL参数,并为SEO友好型内容提供了一些空间,可以将其作为唯一标识符加倍。结构看起来像这样:
http://www.site.com/controller/seo-content/action/myAction/param/val/param2/val2
例如,假设您的网站的基本URL是:
http://www.cleverna.me
默认路由控制器是index ,默认操作是handler() ,因此上述等同于以下内容:
http://www.cleverna.me/index/action/handler
如果您有article路由控制器,则可以这样要求:
http://www.cleverna.me/article
公民通过了由名称/值对组成的URL参数,而不是查询字符串。如果您必须通过237的文章ID和2页数,则将名称/值对附加到URL:
http://www.cleverna.me/article/id/237/page/2
有效的参数名称可能包含字母,数字,下划线和破折号,但必须以字母或下划线开始。
默认控制器操作是handler() ,但是您可以使用action参数指定替代操作(稍后再详细介绍):
http://www.cleverna.me/article/action/edit
公民还可以选择将相关内容插入您的URL,例如:
http://www.cleverna.me/article/My-Clever-Article-Title/page/2
此SEO内容必须始终遵循控制器名称,并在任何名称/值对之前,包括控制器操作。您可以通过route.descriptor或在url范围内(在这种情况下在url.article )中访问它,这意味着您可以将其用作唯一标识符(有关路由控制器部分中的URL参数的更多信息)。
URL参数action和direct为框架保留,因此请勿将其用于应用程序。
公民依靠一个简单的模型观察器惯例。上面提到的文章模式可能使用以下结构:
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
给定URL需要至少一个路由控制器,并且路由控制器的默认视图文件必须共享其名称。模型是可选的。
给定路由控制器的所有视图都可以在app/views/目录中存在,也可以将其放置在其名称与清洁组织控制器的名称匹配的目录中:
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html // The default view
edit.html // Alternate article views
delete.html
有关视图部分中的视图的更多信息。
模型和视图是可选的,不一定需要与特定控制器相关联。如果您的路由控制器要将其输出传递给另一个控制器以进行进一步处理和最终渲染,则无需包括匹配视图(请参阅Controller Next Next指令)。
公民路线控制器只是一个JavaScript模块。每个路由控制器至少需要一个导出作为所请求路线的动作。默认操作应命名为handler() ,当URL中未指定任何诉讼时,公民称其为公民。
// Default route controller action
export const handler = async ( params , request , response , context ) => {
// Do some stuff
return {
// Send content and directives to the server
}
} Citizen Server处理handler()处理初始请求并传递它4个参数:一个包含请求参数的params对象,Node.js request和response对象以及当前请求的上下文。
params对象的属性config | 您的应用程序的配置,包括当前控制器操作的任何自定义 |
route | 所请求的路线的详细信息,例如URL和路由控制器的名称 |
url | 从URL得出的任何参数 |
form | 从帖子收集的数据 |
payload | 原始请求有效载荷 |
cookie | 随着请求发送的饼干 |
session | 会话变量,如果启用了会话 |
除了可以在您的控制器中访问这些对象外,它们还自动包含在视图上下文中,因此您可以将它们在视图模板中引用为本地变量(“视图部分中的更多详细信息)。
例如,基于上一篇文章URL ...
http://www.cleverna.me/article/My-Clever-Article-Title/id/237/page/2
...您将拥有以下params.url对象传递给您的控制器:
{
article : 'My-Clever-Article-Title' ,
id : '237' ,
page : '2'
}控制器名称成为引用描述符的URL范围中的属性,这使其非常适合用作唯一标识符。它也可以在params.route对象中作为params.route.descriptor提供。
context参数包含链中先前控制器生成的任何数据或指令使用其return语句。
要返回控制器诉讼的结果,请包括一个return声明,其中包含要传递给公民的任何数据和指令。
使用上述URL参数,我可以从模型中检索文章内容并将其传递回服务器:
// 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
}
}
}可以使用action URL参数请求替代操作。例如,也许我们希望采取不同的动作并查看来编辑文章:
// 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'
}
}您将要在return声明中传递给公民的所有数据。您要在视图中呈现的所有数据都应传递给一个称为local对象的公民,如上所示。可以将其他对象传递给公民,以设置为服务器提供指令的指令(请参阅控制器指令)。您甚至可以将自己的对象添加到上下文中,并将它们从控制器传递到控制器(更多的控制器链条部分。)。
模型是可选的模块,它们的结构完全取决于您。公民不会直接与您的模型交谈;为了您的方便,它仅将它们存储在app.models中。如果愿意,您也可以手动将它们导入控制器。
将以下函数放置在app/models/article.js中时,将通过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
}默认情况下,公民使用模板文字来查看渲染。您可以安装consolidate.js并使用任何受支持的模板引擎。只需相应地更新templateEngine配置设置。
在article.html中,您可以引用将放置在路由控制器的返回语句中的local对象中的变量。公民还会自动将来自params对象的属性从参数对象注入您的视图上下文,因此您可以访问这些对象作为局部变量(例如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 > 默认情况下,服务器呈现其名称匹配控制器的视图。要呈现不同的视图,请在返回语句中使用view指令。
所有视图都进入/app/views 。如果控制器具有多个视图,则可以在以该控制器命名的目录中组织它们。
app/
controllers/
routes/
article.js
index.js
views/
article/
article.html // Default article controller view
edit.html
index.html // Default index controller view
您可以通过在请求中设置适当的HTTP Accept标头来告诉路由控制器作为JSON或JSON-P返回其本地变量,让相同的资源同时提供完整的HTML视图,也可以使用AJAX请求和RESTFULE API的JSON。
文章路由控制器handler()操作将返回:
{
"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] "
}
}无论您在控制器的返回语句local对象中添加的任何内容都将被返回。
对于JSONP,请在URL中使用callback :
http://www.cleverna.me/article/My-Clever-Article-Title/callback/foo
返回:
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]"
}
} ) ;要强制给定请求的特定内容类型,请在路由控制器中设置response.contentType 。
export const handler = async ( params , request , response ) => {
// Every request will receive a JSON response regardless of the Accept header
response . contentType = 'application/json'
}您可以在事件钩中的所有请求中强制全局响应类型。
助手是可选的实用程序模块,它们的结构完全取决于您。为了您的方便,它们存储在app.helpers中。如果愿意,您也可以手动将它们导入控制器和模型。
将以下功能放置在app/helpers/validate.js中时,将通过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在app范围中存储所有模块,不仅是为了轻松检索,还可以支持热模块更换(HMR)。当您将更改保存到开发模式下的任何模块或视图时,公民会实时清除现有的模块导入和重新导出。
您将看到一个记录的控制台记录,并记录了受影响的文件,并且您的应用程序将继续运行。无需重新启动。
公民尽力在不退出过程的情况下优雅地处理错误。以下控制器操作将引发错误,但服务器将以500响应并继续运行:
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
}
}您还可以手动投掷错误并自定义错误消息:
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
}
}请注意, params.route.controller已从请求的控制器更新为error ,因此您的应用程序中的任何引用都应考虑到所请求的控制器。
以路线要求的格式返回错误。如果您请求JSON,并且该路线会引发错误,则公民将以JSON格式返回错误。
脚手架实用程序创建的应用程序骨架包括用于通用客户端和服务器错误的可选错误视图模板,但是您可以为任何HTTP错误代码创建模板。
公民的默认错误处理方法是capture ,它尝试优雅地恢复。如果您希望在错误后退出该过程,请更改config.citizen.errors exit 。
// config file: exit the process after an error
{
"citizen" : {
"errors" : "exit"
}
}申请错误处理程序发射后,公民将退出该过程。
要为服务器错误创建自定义错误视图,请创建一个名称为/app/views/error目录,并使用以HTTP响应代码或节点错误代码命名的模板填充它。
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
除了查看数据外,路由控制器操作的返回语句还可以将指令传递到呈现替代视图,设置cookie和会话变量,启动重定向,呼叫和渲染包括,缓存路由控制器操作/视图(或整个请求),并将请求移交给另一个控制器以进行进一步处理。
默认情况下,服务器呈现其名称匹配控制器的视图。要呈现不同的视图,请在返回语句中使用view指令:
// 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'
}
}您通过在控制器操作中返回cookie对象来设置cookie。
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
}
}
}
}这是一个完整的cookie对象的默认设置的示例:
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
}一旦在客户端上设置了cookie,它们就可以在params.cookie中提供,而在视图中只需cookie :
<!doctype html >
< html >
< body >
< section >
Welcome, ${cookie.username}.
</ section >
</ body >
</ html >您在控制器中设置的cookie变量在params.cookie范围内没有立即可用。公民必须从控制器中接收上下文,并首先将响应发送给客户端,因此,如果您在相同的请求中需要访问该变量的本地实例。
公民设定的所有饼干都从ctzn_前缀开始,以避免发生冲突。不要使用ctzn_启动您的cookie名称,您应该没有问题。
如果您使用代理后面的公民,例如Nginx或Apache,请确保您的服务器配置中有HTTP Forwarded标头,以便公民处理安全cookies的操作正常。
这是您如何在nginx中设置此操作的示例:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:8080;
}
如果启用了会话,则可以通过您的控制器中的params.session访问会话变量,也可以简单地在视图中访问会话session 。这些本地范围引用当前用户的会话,而无需传递会话ID。
默认情况下,会话具有四个属性: id , started , expires和timer 。会话ID还以称为ctzn_session_id的cookie发送给客户端。
设置会话变量与设置cookie变量几乎相同:
return {
session : {
username : 'Danny' ,
nickname : 'Doc'
}
}像cookie一样,您刚刚分配的会话变量在params.session范围内的相同请求中不可用,因此,如果您需要立即访问此数据,请使用本地实例。
会话基于sessions.lifespan config属性到期,该属性表示会话的长度。默认值为20分钟。 timer是用户的每个请求重置的。 timer用完时,会话将删除。在此之后的任何客户端请求都将生成新的会话ID,并向客户端发送新的会话ID cookie。
强行清除并过期当前用户的会话:
return {
session : {
expires : 'now'
}
} 公民设置的所有会话变量从ctzn_前缀开始,以避免发生冲突。不要使用ctzn_启动会话变量名称,您应该没有问题。
您可以将重定向指令传递到处理控制器操作后将启动的服务器。
redirect对象在其速记版本中采用一个URL字符串,或三个选项: statusCode , url和refresh 。如果您不提供状态代码,则公民使用302(临时重定向)。 refresh选项确定重定向是使用位置标头还是非标准刷新标头。
// 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
}
}与位置标头不同,如果您使用refresh选项,则公民会向客户发送渲染视图,因为重定向是客户端。
使用位置标头断路(我认为)引用器标头,因为引用器最终不是启动重定向的资源,而是启动该页面之前的资源。为了解决此问题,Citizen存储一个称为ctzn_referer的会话变量,其中包含启动重定向的资源的URL,您可以使用该网址正确地重定向用户。例如,如果未经验证的用户尝试访问安全页面,并且您将其重定向到登录表单,则安全页面的地址将存储在ctzn_referer中,因此您可以将其发送到此处而不是上页。
如果您尚未启用会议,则公民会改为创建一个名为ctzn_referer的cookie。
如果您使用代理后面的公民(例如Nginx或Apache),请确保您的服务器配置中具有HTTP Forwarded标头,因此ctzn_referer可以正常工作。
这是您如何在nginx中设置此操作的示例:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:8080;
}
您可以使用header指令设置HTTP标头:
return {
header : {
'Cache-Control' : 'max-age=86400' ,
'Date' : new Date ( ) . toISOString ( )
}
}您还可以使用Node的response.setHeader()方法,但是使用Citizen的header指令将这些标头保存在请求缓存中,因此每当从缓存中取出该控制器操作时,它们都会被应用。
公民让您可以使用完整的MVC模式,即公民版本的组件。每个都有自己的路线控制器,模型和视图。包含可用于执行操作或返回完整的渲染视图。任何路由控制器都可以包括。
假设我们的文章模板的模板具有以下内容。头部包含动态元数据,并且标头的内容根据用户是否登录而变化:
<!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 >使用的主题部分和标题可能是有意义的,因为您可以在任何地方使用该代码,但是您可以创建公民包含的简单局部代码。头部可以使用自己的模型来填充元数据,并且由于已经过身份验证的用户的标头不同,因此让我们将该逻辑从视图中拉出并将其放在标头的控制器中。我喜欢遵循以下典范的惯例,但这取决于您:
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
当文章控制器被解雇时,它必须告诉包括其需要的公民。我们将其与指令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'
}
}
}
}公民包括模式与常规模式相同的要求,包括具有公共行动的控制者。上面的include指令告诉公民呼叫_head和_header控制器,将其传递给已传递给article控制器的参数(参数,请求,响应,上下文),呈现其各自的观点,并将结果视图添加到视图上下文中。
这是我们的总部控制器的外观:
// _head controller
export const article = async ( params ) => {
let metaData = await app . models . _head ( { article : params . url . article } )
return {
local : {
metaData : metaData
}
}
}和头部视图:
< 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 >这是我们的标题控制器的样子:
// _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'
}
}和标题视图:
<!-- /views/_header/_header.html -->
< header >
< a href =" /login " > Login </ a >
</ header > <!-- /views/_header/_header-authenticated.html -->
< header >
< p > Welcome, ${cookie.username} </ p >
</ header >渲染包括存储在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 >公民包含的是独立的,并将其作为完全渲染的观点交付给呼叫控制器。当他们接收相同的数据(URL参数,表单输入,请求上下文等)作为调用控制器,但在include a内部生成的数据并未传递给呼叫者。
就像其他任何路由控制器一样,可以通过HTTP访问旨在用作包含的模式。您可以像这样请求_header控制器,并收到一大堆HTML或JSON作为回应:
http://cleverna.me/_header
这非常适合处理第一个请求服务器端,然后使用客户端库更新内容。
公民提供了丰富的功能,但是它们确实有局限性,并且在某些情况下可能会过度杀伤。
Citizen允许您使用next指令将多个路由控制器从单个请求组成串联。请求的控制器将其数据传递并渲染到后续控制器,添加其自己的数据并呈现自己的视图。
您可以按照您想要的单个请求将尽可能多的路由控制器串在一起。每个路由控制器都将具有其数据并查看输出存储在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
}
}链中的每个控制器都可以访问上一个控制器的上下文和视图。链中的最后一个控制器提供了最终的渲染视图。带有所有网站全局元素的布局控制器是对此的常见用途。
// 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.默认为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.