市民は、ノードの内臓を掘り下げたり、50種類のパッケージで作られたぐらつきのジェンガタワーを一緒にしたりするのではなく、迅速でスケーラブルなWebサイトの構築に興味のある人向けに設計されたMVCベースのWebアプリケーションフレームワークです。
市民を、従来のサーバー側のWebアプリケーション、モジュラーシングルページアプリケーション(SPA)、またはRESTFUL APIの基礎として使用します。
0.9.xから1.0.xへの移行には多くの破壊的な変化がありました。項目別のリストについては、Changelogを参照して、この更新されたドキュメントを徹底的に確認してください。
明らかに、これはどのnpm/github readmeが含めるべきよりもはるかに多くのコンテンツです。このドキュメントのサイトに取り組んでいます。
私は自分のサイトとOriginalTrilogy.comで市民を使用しています。 OT.comは、アプリ/プロセスがクラッシュすることなく数か月間実行される市民の単一インスタンスを実行する30ドルのクラウドホスティングプランで、中程度のトラフィック(毎月数十万回の視聴)を処理します。とても安定しています。
これらのコマンドは、Webアプリの新しいディレクトリを作成し、市民をインストールし、その足場ユーティリティを使用してアプリのスケルトンを作成し、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構成ファイル、スタートアップオプション、および/またはカスタムコントローラー構成を使用して、Citizenアプリを構成できます。
Config Directoryはオプションであり、市民とアプリの両方を駆動する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()を介してStartupでアプリの構成を設定できます。構成ファイルがある場合、起動configは構成ファイルを拡張します。構成ファイルがない場合、起動構成はデフォルトのCitizen Configを拡張します。
// 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 | 弦 | '' | さまざまな環境に異なる構成ファイルをロードするために、市民はサーバーのホスト名にキーとして依存しています。起動時に、市民がサーバーのホスト名に一致するhostキーを備えた構成ファイルを見つけた場合、その構成ファイルを選択します。これは、HTTPサーバーのhostnameと混同しないでください(以下を参照)。 |
| 市民 | |||
mode | 弦 | 最初にNODE_ENVをチェックし、それ以外の場合はproduction | アプリケーションモードは、特定のランタイム動作を決定します。考えられる値は、 productionおよびdevelopmentモードのサイレンスコンソールログです。開発モードでは、詳細なコンソールログ、URLデバッグオプション、およびホットモジュールの交換が可能になります。 |
global | 弦 | app | STARTファイルで市民を初期化するための条約は、フレームワークをグローバル変数に割り当てます。ドキュメント全体で参照されるデフォルトはappです。別の名前を使用する場合は、この設定を変更できます。 |
contentTypes | 配列 | [ 'text/html', 'text/plain', 'application/json', 'application/javascript' ] | クライアントのAcceptリクエストヘッダーに基づいて、各要求の応答形式のAllowlist。個々のルートコントローラーまたはアクションに使用可能なフォーマットを構成する場合、利用可能なフォーマットの配列全体を提供する必要があります。 |
errors | 弦 | capture | アプリケーションがエラーをスローする場合、デフォルトの動作は、市民がエラーから回復し、アプリケーションを実行し続けようとすることです。このオプションを設定してexitと、市民にエラーを記録し、代わりにプロセスを終了するように指示します。 |
templateEngine | 弦 | templateLiterals | 市民は[テンプレートリテラル](https://developer.mozilla.org/en-us/docs/web/javascript/reference/template_literals)ビューレンダリングにデフォルトでレンダリングするために使用します。オプションで、サポートするエンジンを統合して使用できます(たとえば、ハンドルバーをインストールし、 templateEngine handlebarsに設定します)。 |
urlPath | 弦 | / | アプリに通じるURLパスを示します。 http://yoursite.com/my/appからアプリにアクセスできるようにしたい場合、リクエストをプロキシするために別のサーバーをフロントエンドとして使用していない場合、この設定は/my/appでなければなりません(主要なスラッシュを忘れないでください)。この設定は、ルーターが動作するために必要です。 |
| http | |||
enabled | ブール | true | HTTPサーバーを有効にします。 |
hostname | 弦 | 127.0.0.1 | HTTPを介してアプリにアクセスできるホスト名。空の文字列を指定して、任意のホスト名でリクエストを受け入れることができます。 |
port | 番号 | 3000 | 市民のHTTPサーバーがリクエストのために耳を傾けるポート番号。 |
| https | |||
enabled | ブール | false | HTTPSサーバーを有効にします。 |
hostname | 弦 | 127.0.0.1 | HTTPSを介してアプリにアクセスできるホスト名。デフォルトはlocalhostですが、任意のホスト名でリクエストを受け入れる空の文字列を指定できます。 |
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およびデフレート圧縮を有効にします。 |
force | ブールまたは文字列 | false | 圧縮形式の受け入れを報告しなくても、すべてのクライアントのエンコードを強制またはデフレートします。多くのプロキシとファイアウォールは、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 | 市民がデバッグコンテンツにオブジェクトを捨てると、ノードのutil.incpectを使用して検査します。この設定は、検査の深さを決定します。つまり、検査および表示されるノードの数を意味します。数が多いことは、より深い検査とパフォーマンスの低下を意味します。 |
view | ブール | false | これをtrueに設定して、デバッグ情報をHTMLビューに直接ダンプします。 |
enableCache | ブール | false | 開発モードはキャッシュを無効にします。この設定をtrueに変更して、開発モードでキャッシュを有効にします。 |
| Development.Watcher | |||
custom | 配列 | 市民のホットモジュールの交換を指示して、独自のカスタムモジュールを視聴できます。この配列には、 watch (App Directory内のモジュールへの相対ディレクトリパス)およびassign (これらのモジュールを割り当てる変数)のプロパティを備えたオブジェクトを含めることができます。例:[ { "watch": "/util", "assign": "app.util" } ] | |
CitizenはChokidarをファイルウォッチャーとして使用するため、ログと開発モードの両方のwatcherオプションもChokidarで許可されているオプションを受け入れます。
これらの設定は、 app.config.hostおよびapp.config.citizenを介して公開されます。
このドキュメントでは、グローバルアプリ変数名がappであると想定しています。それに応じて調整します。
app.start() | Citizen 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() | 市民が使用する基本的なコンソールとファイルロギングは、使用するためにエクスポートされます。 |
Citizen 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には少なくとも1つのルートコントローラーが必要であり、ルートコントローラーのデフォルトビューファイルはその名前を共有する必要があります。モデルはオプションです。
特定のルートコントローラーのすべてのビューはapp/views/ディレクトリに存在するか、クリーナー組織のコントローラーの名前と一致するディレクトリに配置できます。
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html // The default view
edit.html // Alternate article views
delete.html
ビューセクションの詳細。
モデルとビューはオプションであり、必ずしも特定のコントローラーに関連付ける必要はありません。ルートコントローラーがその出力を別のコントローラーに渡して、さらに処理および最終レンダリングを行う場合は、マッチングビューを含める必要はありません(コントローラーの次のディレクティブを参照)。
市民ルートコントローラーは、JavaScriptモジュールです。各ルートコントローラーでは、要求されたルートのアクションとして機能するために、少なくとも1つのエクスポートが必要です。デフォルトのアクションは、URLでアクションが指定されていない場合に市民によって呼び出されるhandler()という名前にする必要があります。
// 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引数を渡しました。リクエストのパラメーター、node.js requestとresponseオブジェクト、および現在のリクエストのコンテキストを含むparamsオブジェクト。
paramsオブジェクトのプロパティconfig | 現在のコントローラーアクションのカスタマイズを含む、アプリの構成 |
route | URLやルートコントローラーの名前など、要求されたルートの詳細 |
url | URLから派生したパラメーター |
form | 投稿から収集されたデータ |
payload | 生のリクエストペイロード |
cookie | リクエストで送信された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.descriptorとしてparams.routeオブジェクトでも利用できます。
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では、ルートコントローラーのreturnステートメントに渡された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 > デフォルトでは、サーバーは、名前がコントローラーの名前と一致するビューをレンダリングします。別のビューをレンダリングするには、returnステートメントで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 、同じリソースをAjaxリクエストとREST FULD APIのJSONの両方に提供することにより、ROATEコントローラーにローカル変数をJSONまたはJSON-Pとして返すように指示できます。
記事ルートコントローラー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] "
}
}コントローラーのreturnステートメントに追加したものはすべて、 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 )
} 市民は、簡単に検索するだけでなく、ホットモジュールの交換(HMR)をサポートするためだけでなく、 app範囲内にすべてのモジュールを保存します。変更モードのモジュールまたはビューへの変更を保存すると、市民は既存のモジュールのインポートをクリアし、そのモジュールをリアルタイムで再輸入します。
影響を受けるファイルに記載されているコンソールログが表示され、アプリは引き続き実行されます。再起動する必要はありません。
市民は、プロセスを終了することなく優雅にエラーを処理するために最善を尽くします。次のコントローラーアクションはエラーをスローしますが、サーバーは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形式のエラーを返します。
Scaffoldユーティリティによって作成されたアプリスケルトンには、一般的なクライアントとサーバーエラーのオプションのエラービューテンプレートが含まれていますが、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とセッション変数を設定し、リダイレクトを開始し、コールとレンダリングを含む、キャッシュルートコントローラーのアクション/ビュー(またはリクエスト全体)、およびさらなる処理のために別のコントローラーにリクエストを配ってください。
デフォルトでは、サーバーは、名前がコントローラーの名前と一致するビューをレンダリングします。別のビューをレンダリングするには、returnステートメントで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スコープ内ですぐに使用できません。市民はコントローラーからコンテキストを受け取り、最初にクライアントに応答を送信する必要があるため、同じリクエスト中にアクセスする必要がある場合は、変数のローカルインスタンスを使用してください。
市民によって設定されたすべてのCookieは、衝突を避けるためにctzn_プレフィックスから始まります。 ctzn_でCookieの名前を起動しないでください。問題ないはずです。
NginxやApacheなどのプロキシの背後にある市民を使用する場合は、サーバー構成にHTTP Forwardedヘッダーがあることを確認してください。
これを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に合格することなく現在のユーザーセッションを参照しています。
デフォルトでは、セッションには4つのプロパティがあります。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オブジェクトは、速記バージョンまたは3つのオプションの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は、リダイレクトを開始したリソースのURLを含むctzn_refererと呼ばれるセッション変数を保存します。これは、ユーザーを適切にリダイレクトするために使用できます。たとえば、認可されていないユーザーがセキュアページにアクセスしようとしてログインフォームにリダイレクトした場合、セキュアページのアドレスが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 Directiveでそれを行います。
// 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れるものは、scope:scopeに保存されます。
<!-- /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パラメーター、フォーム入力、要求コンテキストなど)を受信しますが、内部で生成されたデータは、Callerに渡されません。
インクルードとして使用することを目的としたパターンは、他のルートコントローラーと同様にHTTPを介してアクセスできます。 _headerコントローラーのように_headerコントローラーを要求し、応答としてHTMLまたはJSONのチャンクを受け取ることができます。
http://cleverna.me/_header
これは、最初のリクエストサーバー側を処理し、クライアント側のライブラリでコンテンツを更新するのに最適です。
市民には豊富な機能を提供しますが、制限があり、特定の状況ではやり過ぎになる可能性があります。
市民を使用すると、 nextディレクティブを使用して、単一のリクエストから複数のルートコントローラーをシリーズで統合することができます。要求されたコントローラーはデータを渡し、ビューを後続のコントローラーにレンダリングし、独自のデータを追加し、独自のビューをレンダリングします。
1つのリクエストで多くのルートコントローラーを一緒に描くことができます。各ルートコントローラーには、 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. 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.