Citizen adalah kerangka kerja aplikasi web berbasis MVC yang dirancang untuk orang-orang yang tertarik untuk membangun situs web yang cepat dan dapat diskalakan alih-alih menggali di sekitar nyali Node atau menyatukan menara Jenga yang goyah yang terbuat dari 50 paket berbeda.
Gunakan Citizen sebagai dasar untuk aplikasi web sisi server tradisional, aplikasi satu halaman modular (SPA), atau API yang tenang.
Ada banyak perubahan dalam transisi dari 0.9.x ke 1.0.x. Silakan berkonsultasi dengan changelog untuk daftar terperinci dan tinjau dokumentasi yang diperbarui ini secara menyeluruh.
Jelas, ini adalah konten yang jauh lebih banyak daripada yang harus dipenuhi oleh NPM/GitHub Readme. Saya sedang mengerjakan situs untuk dokumentasi ini.
Saya menggunakan warga di situs pribadi saya dan originalTrilogy.com. OT.com menangani sejumlah besar lalu lintas (beberapa ratus ribu tampilan setiap bulan) pada rencana hosting cloud $ 30 yang menjalankan satu contoh warga negara, di mana aplikasi/proses berjalan selama berbulan -bulan pada suatu waktu tanpa mogok. Itu sangat stabil.
Perintah ini akan membuat direktori baru untuk aplikasi web Anda, menginstal warga, menggunakan utilitas perancahnya untuk membuat kerangka aplikasi, dan memulai server web:
$ mkdir myapp && cd myapp
$ npm install citizen
$ node node_modules/citizen/util/scaffold skeleton
$ node app/start.jsJika semuanya berjalan dengan baik, Anda akan melihat konfirmasi di konsol yang dijalankan server web. Buka http://127.0.0.1:3000 di browser Anda dan Anda akan melihat template indeks telanjang.
Citizen menggunakan literal template di mesin template default. Anda dapat menginstal konsolidasi, memperbarui konfigurasi template, dan memodifikasi templat tampilan default yang sesuai.
Untuk opsi konfigurasi, lihat Konfigurasi. Untuk lebih banyak utilitas membantu Anda memulai, lihat utilitas.
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
Impor warga negara dan mulai aplikasi Anda:
// start.js
import citizen from 'citizen'
global . app = citizen
app . start ( )Jalankan dari terminal:
$ node start.jsAnda dapat mengonfigurasi aplikasi warga Anda dengan file konfigurasi, opsi startup, dan/atau konfigurasi pengontrol khusus.
Direktori Config adalah opsional dan berisi file konfigurasi dalam format JSON yang mendorong warga dan aplikasi Anda. Anda dapat memiliki beberapa file konfigurasi warga dalam direktori ini, memungkinkan konfigurasi yang berbeda berdasarkan lingkungan. Citizen membangun konfigurasinya berdasarkan hierarki berikut:
host yang cocok dengan nama host mesin, dan jika menemukan satu, memperluas konfigurasi default dengan konfigurasi file.host yang cocok, ia mencari file bernama Citizen.json dan memuat konfigurasi itu.Katakanlah Anda ingin menjalankan warga di port 8080 di lingkungan dev lokal Anda dan Anda memiliki database lokal yang akan terhubung dengan aplikasi Anda. Anda dapat membuat file konfigurasi yang disebut local.json (atau dev.json, apa pun yang Anda inginkan) dengan yang berikut:
{
"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
}
}Konfigurasi ini akan memperluas konfigurasi default hanya saat berjalan di mesin lokal Anda. Dengan menggunakan metode ini, Anda dapat melakukan beberapa file konfigurasi dari lingkungan yang berbeda ke repositori yang sama.
Pengaturan basis data akan dapat diakses di mana saja dalam aplikasi Anda melalui app.config.db . Node citizen dan host dicadangkan untuk kerangka kerja; Buat simpul Anda sendiri untuk menyimpan pengaturan khusus Anda.
Anda dapat mengatur konfigurasi aplikasi Anda saat startup melalui app.start() . Jika ada file konfigurasi, konfigurasi startup akan memperluas file konfigurasi. Jika tidak ada file konfigurasi, konfigurasi startup memperluas konfigurasi warga negara default.
// Start an HTTPS server with a PFX file
app . start ( {
citizen : {
http : {
enabled : false
} ,
https : {
enabled : true ,
pfx : '/absolute/path/to/site.pfx'
}
}
} ) Untuk mengatur konfigurasi khusus di tingkat pengontrol rute, ekspor objek config (lebih lanjut pada pengontrol rute dan tindakan di bagian Route Controllers).
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
}
}
} Berikut ini mewakili konfigurasi default warga, yang diperluas dengan konfigurasi Anda:
{
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
}
}
} Berikut ini adalah ikhtisar lengkap pengaturan warga dan apa yang mereka lakukan.
Saat memulai server, selain opsi konfigurasi http dan https Citizen, Anda dapat memberikan opsi yang sama dengan Node's HTTP.CreateServer () dan https.createServer ().
Satu -satunya perbedaan adalah bagaimana Anda meneruskan file kunci. Seperti yang dapat Anda lihat dalam contoh -contoh di atas, Anda melewati jalur file warga untuk file kunci Anda. Citizen membaca file untuk Anda.
| Pengaturan | Jenis | Nilai default | Keterangan |
|---|---|---|---|
host | Rangkaian | '' | Untuk memuat file konfigurasi yang berbeda di lingkungan yang berbeda, warga bergantung pada nama host server sebagai kunci. Saat startup, jika Citizen menemukan file konfigurasi dengan kunci host yang cocok dengan nama host server, ia memilih file konfigurasi itu. Ini tidak menjadi bingung dengan hostname server http (lihat di bawah). |
| warga negara | |||
mode | Rangkaian | Memeriksa NODE_ENV terlebih dahulu, jika tidak production | Mode aplikasi menentukan perilaku runtime tertentu. Nilai -nilai yang mungkin adalah mode production dan development produk produksi konsol. Mode pengembangan memungkinkan log konsol verbose, opsi debug URL, dan penggantian modul panas. |
global | Rangkaian | app | Konvensi untuk menginisialisasi warga di file start menetapkan kerangka kerja ke variabel global. Standarnya, yang akan Anda lihat direferensikan di seluruh dokumentasi, adalah app . Anda dapat mengubah pengaturan ini jika Anda ingin menggunakan nama lain. |
contentTypes | Array | [ 'text/html', 'text/plain', 'application/json', 'application/javascript' ] | Izin format respons untuk setiap permintaan, berdasarkan header permintaan Accept klien. Saat mengkonfigurasi format yang tersedia untuk pengontrol atau tindakan rute individual, seluruh array format yang tersedia harus disediakan. |
errors | Rangkaian | capture | Ketika aplikasi Anda melakukan kesalahan, perilaku default adalah untuk warga negara untuk mencoba pulih dari kesalahan dan menjaga aplikasi tetap berjalan. Menetapkan opsi ini untuk exit memberitahu warga negara untuk mencatat kesalahan dan keluar dari proses sebagai gantinya. |
templateEngine | Rangkaian | templateLiterals | Citizen menggunakan [templat literal] (https://developer.mozilla.org/en-us/docs/web/javascript/reference/template_literals) Sintaks untuk tampilan rendering secara default. Secara opsional, Anda dapat menginstal konsolidasi dan menggunakan mesin apa pun yang didukungnya (misalnya, instal setang dan atur templateEngine ke handlebars ). |
urlPath | Rangkaian | / | Menunjukkan jalur URL yang mengarah ke aplikasi Anda. Jika Anda ingin aplikasi Anda dapat diakses melalui http://yoursite.com/my/app dan Anda tidak menggunakan server lain sebagai ujung depan untuk proxy permintaan, pengaturan ini harus /my/app (jangan lupa tebasan terkemuka). Pengaturan ini diperlukan agar router berfungsi. |
| http | |||
enabled | Boolean | true | Mengaktifkan server HTTP. |
hostname | Rangkaian | 127.0.0.1 | Nama host di mana aplikasi Anda dapat diakses melalui HTTP. Anda dapat menentukan string kosong untuk menerima permintaan pada nama host apa pun. |
port | Nomor | 3000 | Nomor port di mana server HTTP Citizen mendengarkan permintaan. |
| https | |||
enabled | Boolean | false | Mengaktifkan server HTTPS. |
hostname | Rangkaian | 127.0.0.1 | Nama host di mana aplikasi Anda dapat diakses melalui HTTPS. Standarnya adalah localhost, tetapi Anda dapat menentukan string kosong untuk menerima permintaan di nama host apa pun. |
port | Nomor | 443 | Nomor port di mana server HTTPS Citizen mendengarkan permintaan. |
secureCookies | Boolean | true | Secara default, semua cookie yang ditetapkan dalam permintaan HTTPS aman. Atur opsi ini ke false untuk mengganti perilaku itu, membuat semua cookie tidak aman dan mengharuskan Anda untuk mengatur opsi secure secara manual dalam arahan cookie. |
connectionQueue | Bilangan bulat | null | Jumlah maksimum permintaan yang masuk untuk mengantri. Jika tidak ditentukan, sistem operasi menentukan batas antrian. |
| sesi | |||
enabled | Boolean | false | Mengaktifkan ruang lingkup sesi pengguna, yang memberikan setiap pengunjung ID unik dan memungkinkan Anda untuk menyimpan data yang terkait dengan ID tersebut di dalam server aplikasi. |
lifespan | Bilangan bulat positif | 20 | Jika sesi diaktifkan, angka ini mewakili panjang sesi pengguna, dalam hitungan menit. Sesi secara otomatis kedaluwarsa jika pengguna tidak aktif untuk jumlah waktu ini. |
| tata letak | |||
controller | Rangkaian | '' | Jika Anda menggunakan pengontrol tata letak global, Anda dapat menentukan nama pengontrol itu di sini alih -alih menggunakan arahan next di semua pengontrol Anda. |
view | Rangkaian | '' | Secara default, pengontrol tata letak akan menggunakan tampilan tata letak default, tetapi Anda dapat menentukan tampilan yang berbeda di sini. Gunakan nama file tanpa ekstensi file. |
| bentuk | |||
enabled | Boolean | true | Citizen menyediakan pemrosesan muatan dasar untuk formulir sederhana. Jika Anda lebih suka menggunakan paket formulir yang terpisah, atur ini ke false . |
maxPayloadSize | Bilangan bulat positif | 524288 | Ukuran payload bentuk maksimum, dalam byte. Tetapkan ukuran muatan maks untuk mencegah server Anda kelebihan beban dengan data input formulir. |
| kompresi | |||
enabled | Boolean | false | Memungkinkan gzip dan mengempiskan kompresi untuk tampilan yang diberikan dan aset statis. |
force | Boolean atau string | false | Memaksa GZIP atau mengempiskan pengkodean untuk semua klien, bahkan jika mereka tidak melaporkan menerima format terkompresi. Banyak proxy dan firewall memecah header penerimaan yang menentukan dukungan GZIP, dan karena semua klien modern mendukung GZIP, biasanya aman untuk memaksanya dengan mengatur ini ke gzip , tetapi Anda juga dapat memaksa deflate . |
mimeTypes | Array | Lihat konfigurasi default di atas. | Array jenis mime yang akan dikompresi jika kompresi diaktifkan. Lihat konfigurasi sampel di atas untuk daftar default. Jika Anda ingin menambah atau menghapus item, Anda harus mengganti array secara keseluruhan. |
| cache | |||
control | Objek yang mengandung pasangan kunci/nilai | {} | Gunakan pengaturan ini untuk mengatur header kontrol cache untuk pengontrol rute dan aset statis. Kuncinya adalah nama pathname dari aset, dan nilainya adalah header kontrol cache. Lihat caching sisi klien untuk detailnya. |
invalidUrlParams | Rangkaian | warn | Opsi Route Cache dapat menentukan parameter URL yang valid untuk mencegah URL buruk dari di-cache, dan invalidUrlParams menentukan apakah akan mencatat peringatan saat menghadapi URL buruk atau melemparkan kesalahan sisi klien. Lihat Permintaan Caching dan Tindakan Pengontrol untuk detailnya. |
| Cache.Application | |||
enabled | Boolean | true | Mengaktifkan cache dalam memori, diakses melalui metode cache.set() dan cache.get() . |
lifespan | Nomor | 15 | Lamanya waktu aset aplikasi yang di -cache tetap ada dalam memori, dalam hitungan menit. |
resetOnAccess | Boolean | true | Menentukan apakah akan mengatur ulang pengatur waktu cache pada aset yang di -cache setiap kali cache diakses. Ketika diatur ke false , barang -barang yang di -cache berakhir ketika lifespan tercapai. |
encoding | Rangkaian | utf-8 | Saat Anda melewati jalur file ke cache.set (), pengaturan pengkodean menentukan pengkodean apa yang harus digunakan saat membaca file. |
synchronous | Boolean | false | Saat Anda melewati jalur file ke cache.set (), pengaturan ini menentukan apakah file harus dibaca secara serempak atau asinkron. Secara default, bacaan file tidak sinkron. |
| Cache.Static | |||
enabled | Boolean | false | Saat menyajikan file statis, warga negara biasanya membaca file dari disk untuk setiap permintaan. Anda dapat mempercepat file statis yang melayani dengan mengatur ini ke true , yang menyimpan buffer file dalam memori. |
lifespan | Nomor | 15 | Lamanya waktu aset statis yang di -cache tetap dalam memori, dalam hitungan menit. |
resetOnAccess | Boolean | true | Menentukan apakah akan mengatur ulang penghitung waktu cache pada aset statis yang di -cache setiap kali cache diakses. Ketika diatur ke false , barang -barang yang di -cache berakhir ketika lifespan tercapai. |
| log | |||
access | Boolean | false | Mengaktifkan file log HTTP Access. Dinonaktifkan secara default karena log akses dapat meledak dengan cepat dan idealnya harus ditangani oleh server web. |
debug | Boolean | false | Mengaktifkan file log debug. Berguna untuk men -debug masalah produksi, tetapi sangat bertele -tele (log yang sama yang akan Anda lihat di konsol dalam mode pengembangan). |
maxFileSize | Nomor | 10000 | Menentukan ukuran file maksimum file log, dalam kilobytes. Ketika batas tercapai, file log diganti namanya dengan cap waktu dan file log baru dibuat. |
| Logs.error | |||
client | Boolean | true | Mengaktifkan pencatatan kesalahan klien 400 tingkat. |
server | Boolean | false | Mengaktifkan pencatatan kesalahan server/aplikasi tingkat 500. |
status | Boolean | false | Mengontrol apakah pesan status harus dicatat ke konsol saat dalam mode produksi. (Mode pengembangan selalu masuk ke konsol.) |
| Logs.Watcher | |||
interval | Nomor | 60000 | Untuk sistem operasi yang tidak mendukung peristiwa file, timer ini menentukan seberapa sering file log akan disurvei untuk perubahan sebelum pengarsipan, dalam milidetik. |
| perkembangan | |||
| Development.debug | |||
scope | Obyek | Pengaturan ini menentukan lingkup mana yang dicatat dalam output debug dalam mode pengembangan. Secara default, semua lingkup diaktifkan. | |
depth | Bilangan bulat positif | 3 | Ketika warga negara membuang objek dalam konten debug, ia memeriksanya menggunakan util Node. Pengaturan ini menentukan kedalaman inspeksi, yang berarti jumlah node yang akan diperiksa dan ditampilkan. Angka yang lebih besar berarti inspeksi yang lebih dalam dan kinerja yang lebih lambat. |
view | Boolean | false | Atur ini ke true to dump debug info langsung ke tampilan HTML. |
enableCache | Boolean | false | Mode pengembangan menonaktifkan cache. Ubah pengaturan ini menjadi true untuk mengaktifkan cache dalam mode pengembangan. |
| pengembangan.Watcher | |||
custom | Array | Anda dapat memberi tahu penggantian modul panas Citizen untuk menonton modul khusus Anda sendiri. Array ini dapat berisi objek dengan watch (jalur direktori relatif ke modul Anda dalam direktori aplikasi) dan assign (variabel yang Anda tetapkan modul ini) properti. Contoh:[ { "watch": "/util", "assign": "app.util" } ] | |
Citizen menggunakan Chokidar sebagai pengamat file, jadi opsi watcher untuk log dan mode pengembangan juga menerima opsi apa pun yang diizinkan oleh Chokidar.
Pengaturan ini diekspos secara publik melalui app.config.host dan app.config.citizen .
Dokumentasi ini mengasumsikan nama variabel aplikasi global Anda adalah app . Sesuaikan sesuai.
app.start() | Mulai server aplikasi web Citizen. |
app.config | Pengaturan konfigurasi yang Anda berikan saat startup. Pengaturan Citizen ada di dalam app.config.citizen . |
app.controllersapp.modelsapp.views | Tidak mungkin Anda perlu mengakses pengontrol dan tampilan secara langsung, tetapi merujuk app.models alih-alih mengimpor model Anda secara manual manfaat dari penggantian modul panas warga. |
app.helpers | Semua modul helper/utilitas ditempatkan di app/helpers/ diimpor ke objek pembantu. |
app.cache.set()app.cache.get()app.cache.exists()app.cache.clear() | Cache Aplikasi dan Key/Value Store yang digunakan secara internal oleh Citizen, juga tersedia untuk aplikasi Anda. |
app.log() | Konsol dasar dan penebangan file yang digunakan oleh warga negara, diekspor untuk Anda gunakan. |
Struktur URL Citizen menentukan pengontrol rute dan tindakan untuk menembak, melewati parameter URL, dan membuat sedikit ruang untuk konten ramah-SEO yang dapat berfungsi ganda sebagai pengidentifikasi unik. Strukturnya terlihat seperti ini:
http://www.site.com/controller/seo-content/action/myAction/param/val/param2/val2
Misalnya, katakanlah URL dasar situs Anda adalah:
http://www.cleverna.me
Pengontrol rute default adalah index , dan tindakan default adalah handler() , sehingga di atas setara dengan yang berikut:
http://www.cleverna.me/index/action/handler
Jika Anda memiliki pengontrol rute article , Anda akan memintanya seperti ini:
http://www.cleverna.me/article
Alih -alih string kueri, warga negara melewati parameter URL yang terdiri dari pasangan nama/nilai. Jika Anda harus melewati ID artikel 237 dan nomor halaman 2, Anda akan menambahkan pasangan nama/nilai ke URL:
http://www.cleverna.me/article/id/237/page/2
Nama parameter yang valid dapat berisi huruf, angka, garis bawah, dan tanda hubung, tetapi harus dimulai dengan huruf atau garis bawah.
Tindakan pengontrol default adalah handler() , tetapi Anda dapat menentukan tindakan alternatif dengan parameter action (lebih lanjut tentang ini nanti):
http://www.cleverna.me/article/action/edit
Citizen juga memungkinkan Anda secara opsional memasukkan konten yang relevan ke dalam URL Anda, seperti itu:
http://www.cleverna.me/article/My-Clever-Article-Title/page/2
Konten SEO ini harus selalu mengikuti nama pengontrol dan mendahului setiap pasangan nama/nilai, termasuk tindakan pengontrol. Anda dapat mengaksesnya secara umum melalui route.descriptor atau di dalam lingkup url ( url.article dalam kasus ini), yang berarti Anda dapat menggunakannya sebagai pengidentifikasi unik (lebih lanjut tentang parameter URL di bagian Route Controllers).
action parameter URL dan direct dicadangkan untuk kerangka kerja, jadi jangan gunakan untuk aplikasi Anda.
Citizen mengandalkan konvensi model-view-controller sederhana. Pola artikel yang disebutkan di atas mungkin menggunakan struktur berikut:
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
Setidaknya satu pengontrol rute diperlukan untuk URL yang diberikan, dan file tampilan default rute pengontrol harus berbagi namanya. Model opsional.
Semua tampilan untuk pengontrol rute yang diberikan dapat ada di app/views/ direktori, atau mereka dapat ditempatkan di direktori yang namanya cocok dengan pengontrol untuk organisasi yang lebih bersih:
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html // The default view
edit.html // Alternate article views
delete.html
Lebih lanjut tentang tampilan di bagian tampilan.
Model dan tampilan adalah opsional dan tidak perlu dikaitkan dengan pengontrol tertentu. Jika pengontrol rute Anda akan meneruskan outputnya ke pengontrol lain untuk pemrosesan lebih lanjut dan rendering akhir, Anda tidak perlu menyertakan tampilan yang cocok (lihat arahan controller berikutnya).
Pengontrol rute warga hanyalah modul JavaScript. Setiap pengontrol rute membutuhkan setidaknya satu ekspor untuk berfungsi sebagai tindakan untuk rute yang diminta. Tindakan default harus dinamai handler() , yang dipanggil oleh warga negara ketika tidak ada tindakan yang ditentukan dalam 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 memanggil handler() setelah memproses permintaan awal dan meneruskannya 4 argumen: objek params yang berisi parameter permintaan, objek request dan response Node.js, dan konteks permintaan saat ini.
params config | Konfigurasi aplikasi Anda, termasuk kustomisasi apa pun untuk tindakan pengontrol saat ini |
route | Rincian rute yang diminta, seperti URL dan nama pengontrol rute |
url | Parameter apa pun yang berasal dari URL |
form | Data yang dikumpulkan dari posting |
payload | Muatan permintaan mentah |
cookie | Cookie dikirim dengan permintaan |
session | Variabel sesi, jika sesi diaktifkan |
Selain memiliki akses ke objek -objek ini dalam pengontrol Anda, mereka juga termasuk dalam konteks tampilan Anda secara otomatis sehingga Anda dapat merujuknya dalam templat tampilan Anda sebagai variabel lokal (lebih detail di bagian Tampilan).
Misalnya, berdasarkan URL artikel sebelumnya ...
http://www.cleverna.me/article/My-Clever-Article-Title/id/237/page/2
... Anda akan memiliki objek params.url berikut yang diteruskan ke pengontrol Anda:
{
article : 'My-Clever-Article-Title' ,
id : '237' ,
page : '2'
} Nama pengontrol menjadi properti dalam lingkup URL yang merujuk deskriptor, yang membuatnya sangat cocok untuk digunakan sebagai pengidentifikasi unik. Ini juga tersedia di objek params.route sebagai params.route.descriptor .
Argumen context berisi data atau arahan apa pun yang telah dihasilkan oleh pengontrol sebelumnya dalam rantai menggunakan pernyataan return mereka.
Untuk mengembalikan hasil tindakan pengontrol, sertakan pernyataan return dengan data dan arahan apa pun yang ingin Anda sampaikan kepada warga negara.
Menggunakan parameter URL di atas, saya dapat mengambil konten artikel dari model dan meneruskannya kembali ke server:
// 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
}
}
} Tindakan alternatif dapat diminta menggunakan parameter URL action . Misalnya, mungkin kami ingin tindakan dan tampilan yang berbeda untuk mengedit artikel:
// 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'
}
} Anda menempatkan data apa pun yang ingin Anda lewati ke warga negara dalam pernyataan return . Semua data yang ingin Anda berikan dalam pandangan Anda harus diteruskan ke warga negara dalam suatu objek yang disebut local , seperti yang ditunjukkan di atas. Objek tambahan dapat diteruskan ke warga negara untuk menetapkan arahan yang memberikan instruksi ke server (lihat Arahan Pengontrol). Anda bahkan dapat menambahkan objek Anda sendiri ke konteks dan meneruskannya dari pengontrol ke pengontrol (lebih lanjut di bagian rantai pengontrol.)
Model adalah modul opsional dan strukturnya sepenuhnya terserah Anda. Citizen tidak berbicara dengan model Anda secara langsung; Ini hanya menyimpannya di app.models untuk kenyamanan Anda. Anda juga dapat mengimpornya secara manual ke pengontrol Anda jika Anda mau.
Fungsi berikut, ketika ditempatkan di app/models/article.js , akan dapat diakses di aplikasi Anda melalui app.models.article.get() :
// app.models.article.get()
export const get = async ( id ) => {
let article = // do some stuff to retrieve the article from the db using the provided ID, then...
return article
} Citizen menggunakan literal template untuk tampilan rendering secara default. Anda dapat menginstal Consolidate.js dan menggunakan mesin templat yang didukung. Cukup perbarui pengaturan konfigurasi templateEngine yang sesuai.
Dalam article.html , Anda dapat merujuk variabel yang Anda tempatkan di dalam objek local yang disahkan ke dalam pernyataan pengembalian pengontrol rute. Citizen juga menyuntikkan properti dari objek params ke dalam konteks tampilan Anda secara otomatis, sehingga Anda memiliki akses ke objek -objek tersebut sebagai variabel lokal (seperti ruang lingkup 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 > Secara default, server membuat tampilan yang namanya cocok dengan pengontrol. Untuk memberikan tampilan yang berbeda, gunakan Petunjuk view dalam pernyataan pengembalian Anda.
Semua tampilan masuk /app/views . Jika pengontrol memiliki banyak tampilan, Anda dapat mengaturnya dalam direktori yang dinamai setelah pengontrol itu.
app/
controllers/
routes/
article.js
index.js
views/
article/
article.html // Default article controller view
edit.html
index.html // Default index controller view
Anda dapat memberi tahu pengontrol rute untuk mengembalikan variabel lokalnya sebagai JSON atau JSON-P dengan menetapkan header HTTP Accept yang sesuai dalam permintaan Anda, membiarkan sumber daya yang sama melayani baik tampilan HTML lengkap dan JSON untuk permintaan AJAX dan API yang tenang.
Tindakan handler() artikel akan kembali:
{
"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] "
}
} Apa pun yang telah Anda tambahkan ke pernyataan pengembalian pengontrol, objek local akan dikembalikan.
Untuk JSONP, gunakan callback di URL:
http://www.cleverna.me/article/My-Clever-Article-Title/callback/foo
Kembali:
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]"
}
} ) ; Untuk memaksa jenis konten tertentu untuk permintaan yang diberikan, atur response.contentType di pengontrol rute ke output yang Anda inginkan:
export const handler = async ( params , request , response ) => {
// Every request will receive a JSON response regardless of the Accept header
response . contentType = 'application/json'
}Anda dapat memaksa jenis respons global di semua permintaan dalam kait acara.
Pembantu adalah modul utilitas opsional dan strukturnya sepenuhnya terserah Anda. Mereka disimpan di app.helpers untuk kenyamanan Anda. Anda juga dapat mengimpornya secara manual ke pengontrol dan model Anda jika Anda mau.
Fungsi berikut, ketika ditempatkan di app/helpers/validate.js , akan dapat diakses di aplikasi Anda melalui 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 menyimpan semua modul dalam ruang lingkup app tidak hanya untuk pengambilan yang mudah, tetapi untuk mendukung Hot Module Replacement (HMR). Saat Anda menyimpan perubahan pada modul atau tampilan dalam mode pengembangan apa pun, Citizen membersihkan impor modul yang ada dan mengimpor kembali modul itu secara real time.
Anda akan melihat log konsol yang mencatat file yang terpengaruh, dan aplikasi Anda akan terus berjalan. Tidak perlu restart.
Citizen melakukan yang terbaik untuk menangani kesalahan dengan anggun tanpa keluar dari proses. Tindakan pengontrol berikut akan melakukan kesalahan, tetapi server akan merespons dengan 500 dan terus berjalan:
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
}
}Anda juga dapat melakukan kesalahan secara manual dan menyesuaikan pesan kesalahan:
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
}
} Perhatikan bahwa params.route.controller diperbarui dari pengontrol yang diminta ke error , sehingga setiap referensi dalam aplikasi Anda ke pengontrol yang diminta harus memperhitungkannya.
Kesalahan dikembalikan dalam format yang diminta oleh rute. Jika Anda meminta JSON dan rute melakukan kesalahan, Citizen akan mengembalikan kesalahan dalam format JSON.
Kerangka aplikasi yang dibuat oleh utilitas perancah mencakup templat tampilan kesalahan opsional untuk kesalahan klien dan server umum, tetapi Anda dapat membuat templat untuk kode kesalahan HTTP apa pun.
Metode penanganan kesalahan default Citizen adalah capture , yang mencoba pemulihan yang anggun. Jika Anda lebih suka keluar dari proses setelah kesalahan, ubah config.citizen.errors untuk exit .
// config file: exit the process after an error
{
"citizen" : {
"errors" : "exit"
}
}Setelah penangan kesalahan aplikasi kebakaran, warga akan keluar dari proses.
Untuk membuat tampilan kesalahan khusus untuk kesalahan server, buat direktori yang dipanggil /app/views/error dan mengisinya dengan templat yang dinamai setelah kode respons HTTP atau kode kesalahan node.
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
Selain melihat data, pernyataan pengembalian aksi pengontrol rute juga dapat melewati arahan untuk memberikan tampilan alternatif, mengatur cookie dan variabel sesi, memulai pengalihan, panggilan dan render termasuk, tindakan/tampilan pengontrol rute cache (atau seluruh permintaan), dan menyerahkan permintaan ke pengontrol lain untuk pemrosesan lebih lanjut.
Secara default, server membuat tampilan yang namanya cocok dengan pengontrol. Untuk memberikan tampilan yang berbeda, gunakan Petunjuk view dalam Pernyataan Pengembalian Anda:
// 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'
}
} Anda mengatur cookie dengan mengembalikan objek cookie dalam tindakan pengontrol.
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
}
}
}
}Berikut adalah contoh pengaturan default objek cookie lengkap:
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
} Setelah cookie ditetapkan pada klien, mereka tersedia di params.cookie di dalam pengontrol dan cukup cookie di dalam tampilan:
<!doctype html >
< html >
< body >
< section >
Welcome, ${cookie.username}.
</ section >
</ body >
</ html > Variabel cookie yang Anda atur di dalam pengontrol Anda tidak segera tersedia dalam lingkup params.cookie . Citizen harus menerima konteks dari pengontrol dan mengirim respons ke klien terlebih dahulu, jadi gunakan instance lokal dari variabel jika Anda perlu mengaksesnya selama permintaan yang sama.
Semua cookie yang ditetapkan oleh warga mulai dengan awalan ctzn_ untuk menghindari tabrakan. Jangan memulai nama cookie Anda dengan ctzn_ dan Anda seharusnya tidak memiliki masalah.
Jika Anda menggunakan warga di belakang proxy, seperti Nginx atau Apache, pastikan Anda memiliki header Forwarded HTTP dalam konfigurasi server Anda sehingga penanganan citra warga yang aman berfungsi dengan benar.
Berikut adalah contoh bagaimana Anda dapat mengatur ini di nginx:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:8080;
}
Jika sesi diaktifkan, Anda dapat mengakses variabel sesi melalui params.session di pengontrol Anda atau session hanya dalam tampilan. Lingkup lokal ini merujuk sesi pengguna saat ini tanpa harus melewati ID sesi.
Secara default, sebuah sesi memiliki empat properti: id , started , expires , dan timer . ID sesi juga dikirim ke klien sebagai cookie yang disebut ctzn_session_id .
Variabel sesi pengaturan hampir sama dengan mengatur variabel cookie:
return {
session : {
username : 'Danny' ,
nickname : 'Doc'
}
} Seperti cookie, variabel sesi yang baru saja Anda tetapkan tidak tersedia selama permintaan yang sama dalam lingkup params.session , jadi gunakan instance lokal jika Anda perlu mengakses data ini segera.
Sesi kedaluwarsa berdasarkan properti konfigurasi sessions.lifespan . Standarnya adalah 20 menit. timer diatur ulang dengan setiap permintaan dari pengguna. Ketika timer habis, sesi dihapus. Permintaan klien apa pun setelah waktu itu akan menghasilkan ID sesi baru dan mengirim cookie ID sesi baru ke klien.
Untuk secara paksa jelas dan kedaluwarsa sesi pengguna saat ini:
return {
session : {
expires : 'now'
}
} Semua variabel sesi yang ditetapkan oleh warga negara mulai dengan awalan ctzn_ untuk menghindari tabrakan. Jangan memulai nama variabel sesi Anda dengan ctzn_ dan Anda seharusnya tidak memiliki masalah.
Anda dapat meneruskan instruksi pengalihan ke server yang akan dimulai setelah tindakan pengontrol diproses.
Objek redirect mengambil string URL dalam versi singkatannya, atau tiga opsi: statusCode , url , dan refresh . Jika Anda tidak memberikan kode status, warga menggunakan 302 (pengalihan sementara). Opsi refresh menentukan apakah pengalihan menggunakan header lokasi atau header refresh non-standar.
// 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
}
} Berbeda dengan header lokasi, jika Anda menggunakan opsi refresh , Citizen akan mengirim tampilan yang diberikan kepada klien karena pengalihan terjadi di sisi klien.
Menggunakan header lokasi istirahat (menurut saya) header referer karena referer akhirnya bukan sumber daya yang memprakarsai pengalihan, tetapi sumber daya sebelum halaman yang memprakarsai. Untuk mengatasi masalah ini, Citizen menyimpan variabel sesi yang disebut ctzn_referer yang berisi URL sumber daya yang memprakarsai pengalihan, yang dapat Anda gunakan untuk mengarahkan pengguna dengan benar. Misalnya, jika pengguna yang tidak aautentikasi mencoba untuk mengakses halaman yang aman dan Anda mengarahkannya ke formulir login, alamat halaman yang aman akan disimpan di ctzn_referer sehingga Anda dapat mengirimnya ke sana alih -alih halaman sebelumnya.
Jika Anda belum mengaktifkan sesi, Citizen kembali untuk membuat cookie bernama ctzn_referer sebagai gantinya.
Jika Anda menggunakan warga di belakang proxy, seperti Nginx atau Apache, pastikan Anda memiliki header Forwarded HTTP dalam konfigurasi server Anda sehingga ctzn_referer bekerja dengan benar.
Berikut adalah contoh bagaimana Anda dapat mengatur ini di nginx:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:8080;
}
Anda dapat mengatur header http menggunakan arahan header :
return {
header : {
'Cache-Control' : 'max-age=86400' ,
'Date' : new Date ( ) . toISOString ( )
}
} Anda juga dapat mengatur header secara langsung menggunakan metode response.setHeader() header
Citizen memungkinkan Anda menggunakan pola MVC lengkap sebagai termasuk, yang merupakan komponen versi warga. Masing -masing memiliki pengontrol, model, dan tampilan rute sendiri. Termasuk dapat digunakan untuk melakukan tindakan atau mengembalikan tampilan yang diberikan lengkap. Pengontrol rute apa pun dapat disertakan.
Katakanlah template pola artikel kami memiliki konten berikut. Bagian kepala berisi data meta dinamis, dan konten header berubah tergantung pada apakah pengguna masuk atau tidak:
<!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 >Mungkin masuk akal untuk digunakan termasuk untuk bagian kepala dan header karena Anda akan menggunakan kode itu di mana -mana, tetapi alih -alih parsial sederhana, Anda dapat membuat citizen termasuk. Bagian kepala dapat menggunakan modelnya sendiri untuk mengisi data meta, dan karena header berbeda untuk pengguna yang diautentikasi, mari kita tarik logika itu keluar dari tampilan dan masukkan ke dalam pengontrol header. Saya suka mengikuti konvensi memulai parsial dengan garis bawah, tapi itu terserah Anda:
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
Ketika pengontrol artikel dipecat, ia harus memberi tahu warga negara yang mencakup kebutuhannya. Kami melakukannya dengan arahan 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'
}
}
}
} Warga negara termasuk pola memiliki persyaratan yang sama dengan pola reguler, termasuk pengontrol dengan tindakan publik. Arahan include di atas memberi tahu warga negara untuk memanggil _head dan _header CONTROLER, memberikan argumen yang sama yang diteruskan ke pengontrol article (params, permintaan, respons, konteks), memberikan pandangan masing -masing, dan menambahkan pandangan yang dihasilkan ke konteks tampilan.
Inilah penampilan pengontrol bagian kepala kami:
// _head controller
export const article = async ( params ) => {
let metaData = await app . models . _head ( { article : params . url . article } )
return {
local : {
metaData : metaData
}
}
}Dan tampilan bagian kepala:
< 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 >Inilah yang terlihat seperti pengontrol header kami:
// _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'
}
}Dan tajuk tajuk:
<!-- /views/_header/_header.html -->
< header >
< a href =" /login " > Login </ a >
</ header > <!-- /views/_header/_header-authenticated.html -->
< header >
< p > Welcome, ${cookie.username} </ p >
</ header > Termasuk yang diberikan disimpan dalam lingkup 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 >Warga negara termasuk mandiri dan dikirim ke pengontrol panggilan sebagai pandangan yang sepenuhnya diserahkan. Sementara mereka menerima data yang sama (parameter URL, input formulir, konteks permintaan, dll.) Sebagai pengontrol panggilan, data yang dihasilkan di dalam include tidak diteruskan kembali ke penelepon.
Pola yang dimaksudkan untuk digunakan sebagai termasuk dapat diakses melalui HTTP seperti halnya pengontrol rute lainnya. Anda dapat meminta _header controller seperti itu dan menerima sepotong HTML atau JSON sebagai tanggapan:
http://cleverna.me/_header
Ini bagus untuk menangani sisi server permintaan pertama dan kemudian memperbarui konten dengan pustaka sisi klien.
Warga negara termasuk memberikan fungsionalitas yang kaya, tetapi mereka memiliki keterbatasan dan dapat berlebihan dalam situasi tertentu.
Citizen memungkinkan Anda untuk berantai beberapa pengontrol rute bersama secara seri dari satu permintaan menggunakan arahan next . Pengontrol yang diminta memberikan data dan memberikan tampilan ke pengontrol berikutnya, menambahkan data sendiri dan memberikan pandangannya sendiri.
Anda dapat merangkai sebanyak mungkin pengontrol rute dalam satu permintaan seperti yang Anda inginkan. Setiap pengontrol rute akan memiliki data dan melihat output yang disimpan di objek 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
}
}Setiap pengontrol dalam rantai memiliki akses ke konteks dan pandangan pengontrol sebelumnya. Pengontrol terakhir dalam rantai memberikan tampilan final yang diberikan. A layout controller with all your site's global elements is a common use for this.
// The article controller does its thing, then hands off execution to the _layout controller
export const handler = async ( params , request , response , context ) => {
let article = await getArticle ( { id : params . url . id } )
// The context from the previous controller is available to you in the current controller.
if ( context . doSomething ) { // Or, params.route.chain.index.context
await doSomething ( )
}
return {
local : {
article : article
} ,
next : '/_layout'
}
} The rendered view of each controller in the chain is stored in the route.chain object:
<!-- index.html, which is stored in route.chain.index.output -->
< h1 > Welcome, ${local.user.username}! </ h1 >
<!-- article.html, which is stored in route.chain.article.output -->
< h1 > ${local.article.title} </ h1 >
< p > ${local.article.summary} </ p >
< section > ${local.article.text} </ section >The layout controller handles the includes and renders its own view. Because it's the last controller in the chain, this rendered view is what will be sent to the client.
// _layout controller
export const handler = async ( params ) => {
return {
include : {
_head : '/_head' ,
_header : {
controller : '_header' ,
action : params . cookie . username ? 'authenticated' : 'handler'
} ,
_footer : '/_footer
}
}
} <!-- _layout.html -->
<!doctype html >
< html >
${include._head}
< body >
${include._header}
< main >
<!-- You can render each controller's view explicitly -->
${route.chain.index.output}
${route.chain.article.output}
<!-- Or, you can loop over the route.chain object to output the view from each controller in the chain -->
${Object.keys(route.chain).map( controller = > { return route.chain[controller].output }).join('')}
</ main >
${include._footer}
</ body >
</ html > You can skip rendering a controller's view in the chain by setting the view directive to false:
// This controller action won't render a view
export const handler = async ( ) => {
return {
view : false ,
next : '/_layout'
}
} To bypass next in a request, add /direct/true to the URL.
http://cleverna.me/index/direct/true
The requested route controller's next directive will be ignored and its view will be returned to the client directly.
As mentioned in the config section at the beginning of this document, you can specify a default layout controller in your config so you don't have to insert it at the end of every controller chain:
{
"citizen" : {
"layout" : {
"controller" : " _layout " ,
"view" : " _layout "
}
}
} If you use this method, there's no need to use next for the layout. The last controller in the chain will always hand the request to the layout controller for final rendering.
citizen provides several ways for you to improve your app's performance, most of which come at the cost of system resources (memory or CPU).
In many cases, a requested URL or route controller action will generate the same view every time based on the same input parameters, so it doesn't make sense to run the controller chain and render the view from scratch for each request. citizen provides flexible caching capabilities to speed up your server side rendering via the cache directive.
If a given request (URL) will result in the exact same rendered view with every request, you can cache that request with the request property. This is the fastest cache option because it pulls a fully rendered view from memory and skips all controller processing.
Let's say you chain the index, article, and layout controllers like we did above. If you put the following cache directive in your index controller, the requested URL's response will be cached and subsequent requests will skip the index, article, and layout controllers entirely.
return {
next : '/article' ,
cache : {
request : true
}
}For the request cache directive to work, it must be placed in the first controller in the chain; in other words, the original requested route controller (index in this case). It will be ignored in any subsequent controllers.
The URL serves as the cache key, so each of the following URLs would generate its own cache item:
http://cleverna.me/article
http://cleverna.me/article/My-Article
http://cleverna.me/article/My-Article/page/2
The example above is shorthand for default cache settings. The cache.request directive can also be an object with options:
// Cache the requested route with some additional options
return {
cache : {
request : {
// Optional. This setting lets the server respond with a 304 Not Modified
// status if the cache content hasn't been updated since the client last
// accessed the route. Defaults to the current time if not specified.
lastModified : new Date ( ) . toISOString ( ) ,
// Optional. List of valid URL parameters that protects against accidental
// caching of malformed URLs.
urlParams : [ 'article' , 'page' ] ,
// Optional. Life of cached item in minutes. Default is 15 minutes.
// For no expiration, set to 'application'.
lifespan : 15 ,
// Optional. Reset the cached item's expiration timer whenever the item is
// accessed, keeping it in the cache until traffic subsides. Default is true.
resetOnAccess : true
}
}
} If a given route chain will vary across requests, you can still cache individual controller actions to speed up rendering using the action property.
// Cache this controller action using the default settings
return {
cache : {
action : true
}
}
// Cache this controller with additional options
return {
cache : {
action : {
// These options function the same as request caching (see above)
urlParams : [ 'article' , 'page' ] ,
lifespan : 15 ,
resetOnAccess : true
}
}
}When you cache controller actions, their context is also cached. Setting a cookie or session variable in a cached controller action means all future requests for that action will set the same cookie or session variable—probably not something you want to do with user data.
lastModified This setting lets the server respond with a faster 304 Not Modified response if the content of the request cache hasn't changed since the client last accessed it. By default, it's set to the time at which the request was cached, but you can specify a custom date in ISO format that reflects the last modification to the request's content.
return {
next : '/_layout' ,
cache : {
request : {
// Use toISOString() to format your date appropriately
lastModified : myDate . toISOString ( ) // 2015-03-05T08:59:51.491Z
}
}
} urlParams The urlParams property helps protect against invalid cache items (or worse: an attack meant to flood your server's resources by overloading the cache).
return {
next : '/_layout' ,
cache : {
request : {
urlParams : [ 'article' , 'page' ]
}
}
}If we used the example above in our article controller, the following URLs would be cached because the "article" and "page" URL parameters are permitted:
http://cleverna.me/article
http://cleverna.me/article/My-Article-Title
http://cleverna.me/article/My-Article-Title/page/2
The following URLs wouldn't be cached, which is a good thing because it wouldn't take long for an attacker's script to loop over a URL and flood the cache:
http://cleverna.me/article/My-Article-Title/dosattack/1
http://cleverna.me/article/My-Article-Title/dosattack/2
http://cleverna.me/article/My-Article-Title/page/2/dosattack/3
The server logs a warning when invalid URL parameters are present, but continues processing without caching the result.
lifespanThis setting determines how long the request or controller action should remain in the cache, in minutes.
return {
cache : {
request : {
// This cached request will expire in 10 minutes
lifespan : 10
}
}
} resetOnAccess Used with the lifespan setting, resetOnAccess will reset the timer of the route or controller cache whenever it's accessed, keeping it in the cache until traffic subsides. Defaults to true .
return {
cache : {
request : {
// This cached request will expire in 10 minutes, but if a request accesses it
// before then, the cache timer will be reset to 10 minutes from now
lifespan : 10 ,
resetOnAccess : true
}
}
} In most cases, you'll probably want to choose between caching an entire request (URL) or caching individual controller actions, but not both.
When caching an include controller action, the route pathname pointing to that include is used as the cache key. If you use logic to render different views using the same controller action, the first rendered view will be cached. You can pass an additional URL parameter in such cases to get past this limitation and create a unique cache item for different include views.
export const handler = async ( context ) => {
return : {
// Two different versions of the _header include will be cached becaues the URLs are unique
include : context . authenticated ? '/_header/authenticated/true' : '/_header'
}
} citizen's cache is a RAM cache stored in the V8 heap, so be careful with your caching strategy. Use the lifespan and resetOnAccess options so URLs that receive a lot of traffic stay in the cache, while less popular URLs naturally fall out of the cache over time.
By caching static assets in memory, you speed up file serving considerably. To enable static asset caching for your app's public (web) directory, set cache.static.enabled to true in your config:
{
"citizen" : {
"cache" : {
"static" : {
"enabled" : true
}
}
}
}citizen handles response headers automatically (ETags, 304 status codes, etc.) using each file's last modified date. Note that if a file changes after it's been cached, you'll need to clear the file cache using cache.clear() or restart the app.
To clear a file from the cache in a running app:
app . cache . clear ( { file : '/absolute/path/to/file.jpg' } )With static caching enabled, all static files citizen serves will be cached in the V8 heap, so keep an eye on your app's memory usage to make sure you're not using too many resources.
citizen automatically sets ETag headers for cached requests and static assets. You don't need to do anything to make them work. The Cache-Control header is entirely manual, however.
To set the Cache-Control header for static assets, use the cache.control setting in your config:
{
"citizen" : {
"cache" : {
"static" : true ,
"control" : {
"/css/global.css" : " max-age=86400 " ,
"/css/index.css" : " max-age=86400 " ,
"/js/global.js" : " max-age=86400 " ,
"/js/index.js" : " max-age=86400 " ,
"/images/logo.png" : " max-age=31536000 "
}
}
}
} The key name is the pathname that points to the static asset in your web directory. If your app's URL path is /my/app , then this value should be something like /my/app/styles.css . The value is the Cache-Control header value you want to assign to that asset.
You can use strings that match the exact pathname like above, or you can also use wildcards. Mixing the two is fine:
{
"citizen" : {
"cache" : {
"static" : true ,
"control" : {
"/css/*" : " max-age=86400 " ,
"/js/*" : " max-age=86400 " ,
"/images/logo.png" : " max-age=31536000 "
}
}
}
}Here's a great tutorial on client-side caching to help explain ETag and Cache-Control headers.
Both dynamic routes and static assets can be compressed before sending them to the browser. To enable compression for clients that support it:
{
"citizen" : {
"compression" : {
"enabled" : true
}
}
}Proxies, firewalls, and other network circumstances can strip the request header that tells the server to provide compressed assets. You can force gzip or deflate for all clients like this:
{
"citizen" : {
"compression" : {
"enabled" : true ,
"force" : " gzip "
}
}
}If you have request caching enabled, both the original (identity) and compressed (gzip and deflate) versions of the request will be cached, so your cache's memory utilization will increase.
citizen includes basic request payload parsing. When a user submits a form, the parsed form data is available in your controller via params.form . If you want to use a third-party package to parse the form data yourself, you can disable form parsing in the config and access the raw payload via request.payload .
// login controller
export const handler = ( params ) => {
// Set some defaults for the login view
params . form . username = ''
params . form . password = ''
params . form . remember = false
}
// Using a separate action in your controller for form submissions is probably a good idea
export const submit = async ( params ) => {
let authenticate = await app . models . user . authenticate ( {
username : params . form . username ,
password : params . form . password
} ) ,
cookies = { }
if ( authenticate . success ) {
if ( params . form . remember ) {
cookies . username : authenticate . username
}
return {
cookies : cookies ,
redirect : '/'
}
} else {
return {
local : {
message : 'Login failed.'
}
}
}
}If it's a multipart form containing a file, the form object passed to your controller will look something like this:
{
field1 : 'bar' ,
field2 : 'buzz' ,
fileField1 : {
filename : 'file.png' ,
contentType : 'image/png' ,
binary : < binary data >
}
} File contents are presented in binary format, so you'll need to use Buffer.from(fileField1.binary, 'binary') to create the actual file for storage.
You can pass global form settings via citizen.form in the config or at the controller action level via controller config (see below).
Use the maxPayloadSize config to limit form uploads. The following config sets the maxFieldsSize to 512k:
{
"citizen" : {
"forms" : {
"maxPayloadSize" : 500000 // 0.5MB
}
}
} The maxPayloadSize option includes text inputs and files in a multipart form in its calculations. citizen throws an error if form data exceeds this amount.
Certain events will occur throughout the life of your citizen application, or within each request. You can act on these events, execute functions, set directives, and pass the results to the next event or controller via the context argument. For example, you might set a cookie at the beginning of every new session, or check for cookies at the beginning of every request and redirect the user to a login page if they're not authenticated.
To take advantage of these events, include a directory called "hooks" in your app with any or all of following modules and exports:
app/
controllers/
hooks/
application.js // exports start() and error()
request.js // exports start() and end()
response.js // exports start() and end()
session.js // exports start() and end()
request.start() , request.end() , and response.start() are called before your controller is fired, so the output from those events is passed from each one to the next, and ultimately to your controller via the context argument. Exactly what actions they perform and what they output—content, citizen directives, custom directives—is up to you.
All files and exports are optional. citizen parses them at startup and only calls them if they exist. For example, you could have only a request.js module that exports start() .
Here's an example of a request module that checks for a username cookie at the beginning of every request and redirects the user to the login page if it doesn't exist. We also avoid a redirect loop by making sure the requested controller isn't the login controller:
// app/controllers/hooks/request.js
export const start = ( params ) => {
if ( ! params . cookie . username && params . route . controller !== 'login' ) {
return {
redirect = '/login'
}
}
} session.end is slightly different in terms of the arguments it receives, which consists only of a copy of the expired session (no longer active):
// app/controllers/hooks/session.js
export const end = ( expiredSession ) => {
// do something whenever a session ends
} By default, all controllers respond to requests from the host only. citizen supports cross-domain HTTP requests via access control headers.
To enable cross-domain access for individual controller actions, add a cors object with the necessary headers to your controller's exports:
export const config = {
// Each controller action can have its own CORS headers
handler : {
cors : {
'Access-Control-Allow-Origin' : 'http://www.foreignhost.com' ,
'Access-Control-Expose-Headers' : 'X-My-Custom-Header, X-Another-Custom-Header' ,
'Access-Control-Max-Age' : 600 ,
'Access-Control-Allow-Credentials' : 'true' ,
'Access-Control-Allow-Methods' : 'OPTIONS, PUT' ,
'Access-Control-Allow-Headers' : 'Content-Type' ,
'Vary' : 'Origin'
}
}
} Why not just use the HTTP Headers directive or set them manually with response.setHeader() ? When citizen receives a request from an origin other than the host, it checks for the cors export in your controller to provide a preflight response without you having to write your own logic within the controller action. You can of course check request.method and write logic to handle this manually if you prefer.
For more details on CORS, check out the W3C spec and the Mozilla Developer Network.
If you use citizen behind a proxy, such as NGINX or Apache, make sure you have a Forwarded header in your server configuration so citizen handles CORS requests correctly. Different protocols (HTTPS on your load balancer and HTTP in your citizen app) will cause CORS requests to fail without these headers.
Here's an example of how you might set this up in NGINX:
location / {
proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme;";
proxy_pass http://127.0.0.1:3000;
}
citizen has a built-in application cache where you can store basically anything: strings, objects, buffers, static files, etc.
You can store any object in citizen's cache. The benefits of using cache over storing content in your own global app variables are built-in cache expiration and extension, as well as wrappers for reading, parsing, and storing file content.
citizen's default cache time is 15 minutes, which you can change in the config (see Configuration). Cached item lifespans are extended whenever they're accessed unless you pass resetOnAccess: false or change that setting in the config.
// Cache a string in the default app scope for 15 minutes (default). Keys
// must be unique within a given scope.
app . cache . set ( {
key : 'welcome-message' ,
value : 'Welcome to my site.'
} )
// Cache a string under a custom scope, which is used for retrieving or clearing
// multiple cache items at once. Keys must be unique within a given scope.
// Reserved scope names are "app", "routes", and "files".
app . cache . set ( {
key : 'welcome-message' ,
scope : 'site-messages' ,
value : 'Welcome to our site.'
} )
// Cache a string for the life of the application.
app . cache . set ( {
key : 'welcome-message' ,
value : 'Welcome to my site.' ,
lifespan : 'application'
} )
// Cache a file buffer using the file path as the key. This is a wrapper for
// fs.readFile and fs.readFileSync paired with citizen's cache function.
// Optionally, tell citizen to perform a synchronous file read operation and
// use an encoding different from the default (UTF-8).
app . cache . set ( {
file : '/path/to/articles.txt' ,
synchronous : true ,
encoding : 'CP-1252'
} )
// Cache a file with a custom key. Optionally, parse the JSON and store the
// parsed object in the cache instead of the raw buffer. Expire the cache
// after 10 minutes, regardless of whether the cache is accessed or not.
app . cache . set ( {
file : '/path/to/articles.json' ,
key : 'articles' ,
parseJSON : true ,
lifespan : 10 ,
resetOnAccess : false
} ) app , routes , and files are reserved scope names, so you can't use them for your own custom scopes.
This is a way to check for the existence of a given key or scope in the cache without resetting the cache timer on that item. Returns false if a match isn't found.
// Check for the existence of the specified key
let keyExists = app . cache . exists ( { key : 'welcome-message' } ) // keyExists is true
let keyExists = app . cache . exists ( { file : '/path/to/articles.txt' } ) // keyExists is true
let keyExists = app . cache . exists ( { file : 'articles' } ) // keyExists is true
let keyExists = app . cache . exists ( { key : 'foo' } ) // keyExists is false
// Check the specified scope for the specified key
let keyExists = app . cache . exists ( {
scope : 'site-messages' ,
key : 'welcome-message'
} )
// keyExists is true
// Check if the specified scope exists and contains items
let scopeExists = app . cache . exists ( {
scope : 'site-messages'
} )
// scopeExists is true
// Check if the route cache has any instances of the specified route
let controllerExists = app . cache . exists ( {
route : '/article'
} ) Retrieve an individual key or an entire scope. Returns false if the requested item doesn't exist. If resetOnAccess was true when the item was cached, using retrieve() will reset the cache clock and extend the life of the cached item. If a scope is retrieved, all items in that scope will have their cache timers reset.
Optionally, you can override the resetOnAccess attribute when retrieving a cache item by specifying it inline.
// Retrieve the specified key from the default (app) scope
let welcomeMessage = app . cache . get ( {
key : 'welcome-message'
} )
// Retrieve the specified key from the specified scope and reset its cache timer
// even if resetOnAccess was initially set to false when it was stored
let welcomeMessage = app . cache . get ( {
scope : 'site-messages' ,
key : 'welcome-message' ,
resetOnAccess : true
} )
// Retrieve all keys from the specified scope
let siteMessages = app . cache . get ( {
scope : 'site-messages'
} )
// Retrieve a cached file
let articles = app . cache . get ( {
file : '/path/to/articles.txt'
} )
// Retrieve a cached file with its custom key
let articles = app . cache . get ( {
file : 'articles'
} )Clear a cache object using a key or a scope.
// Store some cache items
app . cache . set ( {
key : 'welcome-message' ,
scope : 'site-messages' ,
value : 'Welcome to our site.'
} )
app . cache . set ( {
key : 'goodbye-message' ,
scope : 'site-messages' ,
value : 'Thanks for visiting!'
} )
app . cache . set ( {
file : '/path/to/articles.txt' ,
synchronous : true
} )
// Clear the welcome message from its custom scope cache
app . cache . clear ( { scope : 'site-messages' , key : 'welcome-message' } )
// Clear all messages from the cache using their custom scope
app . cache . clear ( { scope : 'site-messages' } )
// Clear the articles cache from the file scope
app . cache . clear ( { file : '/path/to/articles.txt' } ) cache.clear() can also be used to delete cached requests and controller actions.
app . cache . clear ( {
route : '/article/My-Article/page/2'
} )
// Clear the entire route scope
app . cache . clear ( { scope : 'routes' } )
// Clear the entire file scope
app . cache . clear ( { scope : 'files' } )
// Clear the entire cache
app . cache . clear ( ) citizen's log() function is exposed for use in your app via app.log() .
Makes it easy to log comments to either the console or a file (or both) in a way that's dependent on the mode of the framework.
When citizen is in production mode, log() does nothing by default. In development mode, log() will log whatever you pass to it. This means you can place it throughout your application's code and it will only write to the log in development mode. You can override this behavior globally with the log settings in your config file or inline with the console or file options when calling log() .
app . log ( {
// Optional. Valid settings are "status" (default) or "error".
type : 'status' ,
// Optional string. Applies a label to your log item.
label : 'Log output' ,
// The content of your log. If it's anything other than a string or
// number, log() will run util.inspect on it and dump the contents.
contents : someObject ,
// Optional. Enables console logs.
console : true ,
// Optional. Enables file logging.
file : false ,
// Optional. File name you'd like to write your log to.
file : 'my-log-file.log' ,
// Optional. Disables the timestamp that normally appears in front of the log
timestamp : false
} ) Log files appear in the directory you specify in config.citizen.directories.logs .
Warning: development mode is inherently insecure. Don't use it in a production environment.
If you set "mode": "development" in your config file, citizen dumps all major operations to the console.
You can also dump the request context to the view by setting development.debug.view in your config file to true , or use the ctzn_debug URL parameter on a per-request basis:
// config file: always dumps debug output in the view
{
"citizen" : {
"development" : {
"debug" : {
"view" : true
}
}
}
} By default, citizen dumps the pattern's complete context. You can specify the exact object to debug with the ctzn_inspect URL parameter:
// Dumps the server params object
http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_inspect/params
// Dumps the user's session scope
http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_inspect/params.session
The debug output traverses objects 4 levels deep by default. To display deeper output, use the development.debug.depth setting in your config file or append ctzn_debugDepth to the URL. Debug rendering will take longer the deeper you go.
// config file: debug 4 levels deep
{
"citizen" : {
"development" : {
"debug" : {
"depth" : 6
}
}
}
}
// URL
// http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_debugDepth/4 In development mode, you must specify the ctzn_debug URL parameter to display debug output. Debug output is disabled in production mode.
The util directory within the citizen package has some helpful utilities.
Creates a complete skeleton of a citizen app with a functional index pattern and error templates.
$ node node_modules/citizen/util/scaffold skeletonResulting file structure:
app/
config/
citizen.json
controllers/
hooks/
application.js
request.js
response.js
session.js
routes/
index.js
models/
index.js
views/
error/
404.html
500.html
ENOENT.html
error.html
index.html
start.js
web/
Run node node_modules/citizen/util/scaffold skeleton -h for options.
Creates a complete citizen MVC pattern. The pattern command takes a pattern name and options:
$ node node_modules/citizen/util/scaffold pattern [options] [pattern] For example, node scaffold pattern article will create the following pattern:
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html
Use node node_modules/citizen/util/scaffold pattern -h to see all available options for customizing your patterns.
(The MIT License)
Copyright (c) 2014-2024 Jay Sylvester
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
By default, citizen dumps the pattern's complete context. You can specify the exact object to debug with the ctzn_inspect URL parameter:
// Dumps the server params object
http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_inspect/params
// Dumps the user's session scope
http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_inspect/params.session
The debug output traverses objects 4 levels deep by default. To display deeper output, use the development.debug.depth setting in your config file or append ctzn_debugDepth to the URL. Debug rendering will take longer the deeper you go.
// config file: debug 4 levels deep
{
"citizen" : {
"development" : {
"debug" : {
"depth" : 6
}
}
}
}
// URL
// http://www.cleverna.me/article/id/237/page/2/ctzn_debug/true/ctzn_debugDepth/4 In development mode, you must specify the ctzn_debug URL parameter to display debug output. Debug output is disabled in production mode.
The util directory within the citizen package has some helpful utilities.
Creates a complete skeleton of a citizen app with a functional index pattern and error templates.
$ node node_modules/citizen/util/scaffold skeletonResulting file structure:
app/
config/
citizen.json
controllers/
hooks/
application.js
request.js
response.js
session.js
routes/
index.js
models/
index.js
views/
error/
404.html
500.html
ENOENT.html
error.html
index.html
start.js
web/
Run node node_modules/citizen/util/scaffold skeleton -h for options.
Creates a complete citizen MVC pattern. The pattern command takes a pattern name and options:
$ node node_modules/citizen/util/scaffold pattern [options] [pattern] For example, node scaffold pattern article will create the following pattern:
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html
Use node node_modules/citizen/util/scaffold pattern -h to see all available options for customizing your patterns.
(The MIT License)
Copyright (c) 2014-2024 Jay Sylvester
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.