Citizen-это платформа веб-приложений на основе MVC, предназначенная для людей, заинтересованных в быстрого создания быстрого масштабируемого, масштабируемых веб-сайтов, вместо того, чтобы копаться вокруг кишки узла или поднимать шаткую башню Jenga, сделанную из 50 различных пакетов.
Используйте Citizen в качестве основы для традиционного веб-приложения на стороне сервера, модульного одностраничного приложения (SPA) или RESTFUL API.
В переходе произошли многочисленные изменения в переходе с 0.9.x до 1,0.x. Пожалуйста, проконсультируйтесь с ChangeLog, чтобы получить подробный список и тщательно просмотрите эту обновленную документацию.
Очевидно, что это гораздо больше контента, чем любой NPM/GitHub Readme. Я работаю на сайте для этой документации.
Я использую Citizen на своем личном сайте и OriginalTricy.com. OT.com обрабатывает умеренное количество трафика (несколько сотен тысяч просмотров каждый месяц) по плану облачного хостинга за 30 долларов США, управляющего одним экземпляром гражданина, где приложение/процесс работает по месяцам без русла. Это очень стабильно.
Эти команды создадут новый каталог для вашего веб -приложения, установите Citizen, используйте его утилиту лесов для создания скелета приложения и запустить веб -сервер:
$ mkdir myapp && cd myapp
$ npm install citizen
$ node node_modules/citizen/util/scaffold skeleton
$ node app/start.jsЕсли все прошло хорошо, вы увидите подтверждение в консоли, которую работает веб -сервер. Зайдите на http://127.0.0.1:3000 в своем браузере, и вы увидите шаблон индекса.
Citizen использует шаблонные литералы в своем шаблонном двигателе по умолчанию. Вы можете установить Consolidate, обновить конфигурацию шаблона и соответствующим образом изменить шаблоны представления по умолчанию.
Для параметров конфигурации см. Конфигурацию. Для получения дополнительной информации, чтобы помочь вам начать работу, см. Утилиты.
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 с помощью файла конфигурации, параметров запуска и/или пользовательских конфигураций контроллера.
Справочник конфигурации является необязательным и содержит файлы конфигурации в формате JSON, которые управляют как Citizen, так и вашим приложением. Вы можете иметь несколько файлов конфигурации Citizen в этом каталоге, позволяющих различным конфигурациям на основе среды. Citizen создает свою конфигурацию на основе следующей иерархии:
host , который соответствует имени хоста машины, и, если он находит один, расширяет конфигурацию по умолчанию с помощью конфигурации файла.host , он ищет файл с именем Citizen.json и загружает эту конфигурацию.Допустим, вы хотите запустить Citizen в порту 8080 в вашей местной среде Dev, и у вас есть локальная база данных, к которой подключается ваше приложение. Вы можете создать файл конфигурации с именем 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() . Если есть файл конфигурации, конфигурация запуска будет расширять файл конфигурации. Если нет файла конфигурации, конфигурация запуска расширяет конфигурацию Citizen по умолчанию.
// 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
}
}
} Следующее представляет конфигурацию по умолчанию Citizen, которая расширяется вашей конфигурацией:
{
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 от Citizen, вы можете предоставить те же параметры, что и http.createserver () и https.createserver ().
Единственная разница в том, как вы передаете ключевые файлы. Как вы можете видеть в примерах выше, вы проходите Citizen Pail Pats для ваших ключевых файлов. Гражданин читает файлы для вас.
| Параметр | Тип | Значение по умолчанию | Описание |
|---|---|---|---|
host | Нить | '' | Чтобы загрузить различные файлы конфигурации в разных средах, Citizen опирается на имя хоста сервера в качестве ключа. При запуске, если Citizen находит файл конфигурации с ключом host , который соответствует имени хоста сервера, он выбирает этот файл конфигурации. Это не следует путать с hostname HTTP -сервера (см. Ниже). |
| гражданин | |||
mode | Нить | Сначала проверяет NODE_ENV , в противном случае production | Режим приложения определяет определенное поведение времени выполнения. Возможные значения - это production и режим development Режим разработки позволяет многословным журналам консоли, вариантам отладки URL и замене горячих модулей. |
global | Нить | app | Конвенция по инициализации гражданина в начальном файле присваивает структуру глобальной переменной. По умолчанию, на который вы будете видеть ссылку на протяжении всей документации, является app . Вы можете изменить этот параметр, если хотите использовать другое имя. |
contentTypes | Множество | [ 'text/html', 'text/plain', 'application/json', 'application/javascript' ] | Alluctlist форматов ответа для каждого запроса, основанный на заголовке запроса Accept клиента. При настройке доступных форматов для отдельных контроллеров или действий маршрутов необходимо предоставить весь массив доступных форматов. |
errors | Нить | capture | Когда ваша приложение выбрасывает ошибку, поведение по умолчанию заключается в том, чтобы гражданин попытался восстановиться после ошибки и сохранить работу приложения. Установка этой опции для exit говорит Citizen, чтобы войти в систему ошибки и вместо этого выйти из процесса. |
templateEngine | Нить | templateLiterals | Citizen использует [Template Literal] (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 -сервер Citizen выслушает запросы. |
| https | |||
enabled | Логический | false | Включает HTTPS -сервер. |
hostname | Нить | 127.0.0.1 | Имя хоста, при котором ваше приложение можно получить через HTTPS. По умолчанию Localhost, но вы можете указать пустую строку для принятия запросов на любом имени хоста. |
port | Число | 443 | Номер порта, на котором HTTPS Server Citizen выслушает запросы. |
secureCookies | Логический | true | По умолчанию все файлы cookie, установленные в рамках запроса HTTPS, безопасны. Установите эту опцию на false , чтобы переопределить это поведение, делая все файлы cookie небезопасными и требуя, чтобы вы вручную установили опцию secure в директиве Cookie. |
connectionQueue | Целое число | null | Максимальное количество входящих запросов в очередь. Если оставлена неопределенной, операционная система определяет предел очереди. |
| сеансы | |||
enabled | Логический | false | Включает объем сеанса пользователя, который назначает каждому посетителю уникальный идентификатор и позволяет хранить данные, связанные с этим идентификатором на сервере приложений. |
lifespan | Положительное целое число | 20 | Если сеансы включены, это число представляет собой длину сеанса пользователя, в минуты. Сеансы автоматически истекают, если пользователь был неактивен в течение этого количества времени. |
| макет | |||
controller | Нить | '' | Если вы используете глобальный контроллер макета, вы можете указать имя этого контроллера здесь вместо использования next директивы во всех ваших контроллерах. |
view | Нить | '' | По умолчанию контроллер макета будет использовать представление макета по умолчанию, но вы можете указать другое представление здесь. Используйте имя файла без расширения файла. |
| формы | |||
enabled | Логический | true | Citizen предоставляет базовую обработку полезной нагрузки для простых форм. Если вы предпочитаете использовать отдельный пакет форм, установите его на false . |
maxPayloadSize | Положительное целое число | 524288 | Максимальный размер полезной нагрузки формы, в байтах. Установите максимальный размер полезной нагрузки, чтобы предотвратить перегрузку вашего сервера входными данными. |
| сжатие | |||
enabled | Логический | false | Включает GZIP и сжатие сдавливания для визуализированных видов и статических активов. |
force | Логический или струнный | false | Приказывает GZIP или дефлятный кодирование для всех клиентов, даже если они не сообщают о принимающих сжатых форматах. Многие прокси и брандмауэры нарушают заголовок при принятии, который определяет поддержку GZIP, и, поскольку все современные клиенты поддерживают GZIP, обычно безопасно заставлять его, установив это для gzip , но вы также можете заставить deflate . |
mimeTypes | Множество | См. Конфигурация по умолчанию выше. | Массив типов MIME, который будет сжат, если включено сжатие. См. Пример конфигурации выше для списка по умолчанию. Если вы хотите добавить или удалить предметы, вы должны заменить массив полностью. |
| кеш | |||
control | Объект, содержащий пары ключей/значения | {} | Используйте этот параметр, чтобы установить заголовки управления кэшем для контроллеров маршрутов и статических активов. Ключ-это путь актива, а значение-заголовок контроля кеша. Смотрите кэширование на стороне клиента для деталей. |
invalidUrlParams | Нить | warn | Опция кэша маршрута может указать допустимые параметры URL, чтобы предотвратить кэширование плохих URL-адресов, а invalidUrlParams определяет, следует ли регистрировать предупреждение при столкновении с плохими URL-адресами или бросить ошибку на стороне клиента. См. Запросы на кэширование и действия контроллера для деталей. |
| cache.pplication | |||
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 | При обслуживании статических файлов Citizen обычно читает файл с диска для каждого запроса. Вы можете значительно ускорить статический файл, обслуживая его на true , который кэширует файловые буферы в памяти. |
lifespan | Число | 15 | Продолжительность времени кэшированный статический актив остается в памяти, за считанные минуты. |
resetOnAccess | Логический | true | Определяет, сбросить таймер кэша на кэшированном статическом активе всякий раз, когда доступ к кэше. При установлении false , кэшированные предметы истекают, когда достигнута lifespan . |
| журналы | |||
access | Логический | false | Включает файлы журнала доступа HTTP. Отключен по умолчанию, потому что журналы доступа могут быстро взорваться, и в идеале его следует обрабатывать веб -сервером. |
debug | Логический | false | Включает файлы журнала отладки. Полезно для отладки вопросов производства, но чрезвычайно многословные (те же журналы, которые вы увидите в консоли в режиме разработки). |
maxFileSize | Число | 10000 | Определяет максимальный размер файла файлов журнала в килобитах. Когда ограничен достигнут, файл журнала переименован с маркой времени, и создается новый файл журнала. |
| logs.error | |||
client | Логический | true | Включает регистрацию ошибок клиента 400 уровней. |
server | Логический | false | Включает журнал 500-уровневых ошибок сервера/приложения. |
status | Логический | false | Управляют, должны ли сообщения о статусе регистрироваться в консоли в режиме производства. (Режим разработки всегда входит в консоль.) |
| logs.watcher | |||
interval | Число | 60000 | Для операционных систем, которые не поддерживают события файлов, этот таймер определяет, как часто файлы журнала будут опробоваться для изменений до архивирования, в миллисекундах. |
| разработка | |||
| Development.debug | |||
scope | Объект | Этот параметр определяет, какие области регистрируются в выводе отладки в режиме разработки. По умолчанию все области включены. | |
depth | Положительное целое число | 3 | Когда Citizen выбросит объект в содержание отладки, он осматривает его, используя util.inspect Node. Эта настройка определяет глубину проверки, что означает количество узлов, которые будут проверены и отображаются. Большие числа означают более глубокий осмотр и более медленную производительность. |
view | Логический | false | Установите это в True, чтобы сбросить информацию отладки непосредственно в представление HTML. |
enableCache | Логический | false | Режим разработки отключает кеш. Измените этот параметр на true , чтобы включить кеш в режиме разработки. |
| Development.watcher | |||
custom | Множество | Вы можете сказать замену горячего модуля Citizen, чтобы посмотреть свои собственные модули. Этот массив может содержать объекты с watch (относительный путь каталога к вашим модулям в каталоге APP) и assign (переменная, которой вы назначаете эти модули). Пример:[ { "watch": "/util", "assign": "app.util" } ] | |
Citizen использует Chokidar в качестве своего наблюдателя за файлом, поэтому опция watcher как для журналов, так и для режима разработки также принимает любую опцию, разрешенную Chokidar.
Эти параметры публично открываются через app.config.host и app.config.citizen .
Эта документация предполагает, что ваше глобальное имя переменной приложения является app . Скорректировать соответствующим образом.
app.start() | Запускается сервер веб -приложений Citizen. |
app.config | Настройки конфигурации, которые вы поставляли при запуске. Настройки граждан находятся в app.config.citizen . |
app.controllersapp.modelsapp.views | Маловероятно, что вам потребуется доступ к контроллерам и представлениям напрямую, но ссылается на app.models вместо импорта ваших моделей вручную выгоду от встроенной замены горячего модуля Citizen. |
app.helpers | Все вспомогательные/коммунальные модули, размещенные в app/helpers/ импортируются в объект помощников. |
app.cache.set()app.cache.get()app.cache.exists()app.cache.clear() | Кэш приложений и магазин ключей/значения, используемые внутри Citizen, также доступны для вашего приложения. |
app.log() | Основная консоль и ведение журнала файлов, используемые гражданином, экспортируется для вашего использования. |
Структура URL граждан определяет, какой контроллер маршрута и действие для стрельбы, пропускает параметры URL и создает немного места для удобного для SEO, который может удвоиться в качестве уникального идентификатора. Структура выглядит так:
http://www.site.com/controller/seo-content/action/myAction/param/val/param2/val2
Например, допустим, базовый URL вашего сайта:
http://www.cleverna.me
Контроллер маршрута по умолчанию является index , а действие по умолчанию - handler() , поэтому вышеупомянутое эквивалент следующего:
http://www.cleverna.me/index/action/handler
Если у вас есть контроллер маршрута article , вы бы запросили его так:
http://www.cleverna.me/article
Вместо строк запроса Citizen проходит параметры URL, состоящие из пар имени/значения. Если бы вам пришлось передать идентификатор статьи 237 и номер страницы 2, вы бы добавили пары имени/значения в URL:
http://www.cleverna.me/article/id/237/page/2
Допустимые имена параметров могут содержать буквы, числа, подчеркивание и тире, но должны начинаться с буквы или подчеркивания.
Действие контроллера по умолчанию - handler() , но вы можете указать альтернативные действия с параметром action (подробнее об этом позже):
http://www.cleverna.me/article/action/edit
Citizen также позволяет вам при желании вставлять соответствующий контент в ваши URL -адреса, например, так:
http://www.cleverna.me/article/My-Clever-Article-Title/page/2
Этот контент SEO всегда должен следовать имени контроллера и предшествовать любым парам имени/значения, включая действие контроллера. Вы можете получить доступ к нему в целом через route.descriptor или в рамках url ( url.article в данном случае), что означает, что вы можете использовать его в качестве уникального идентификатора (подробнее о параметрах URL в разделе контроллеров маршрута).
action параметров URL и direct зарезервированы для структуры, поэтому не используйте их для вашего приложения.
Citizen полагается на простой конвенцию модели-контроллера. Упомянутый выше шаблон статьи может использовать следующую структуру:
app/
controllers/
routes/
article.js
models/
article.js // Optional, name it whatever you want
views/
article.html // The default view file name should match the controller name
По крайней мере, один контроллер маршрута требуется для данного URL -адреса, и файл представления контроллера контроллера маршрута должен использовать свое имя. Модели необязательны.
Все представления для данного контроллера маршрута могут существовать в app/views/ каталоге, или они могут быть помещены в каталог, чье имя соответствует имени контроллера для более чистой организации:
app/
controllers/
routes/
article.js
models/
article.js
views/
article/
article.html // The default view
edit.html // Alternate article views
delete.html
Подробнее о взглядах в разделе «Просмотры».
Модели и представления являются необязательными и не обязательно должны быть связаны с конкретным контроллером. Если ваш контроллер маршрута будет передавать свой вывод в другой контроллер для дальнейшей обработки и окончательного рендеринга, вам не нужно включать соответствующий представление (см. Следующую директиву Controller).
Гражданский контроллер маршрута - это всего лишь модуль JavaScript. Каждому контроллеру маршрута требуется, по крайней мере, один экспорт, чтобы служить действием для запрошенного маршрута. Действие по умолчанию должно быть названо handler() , которое называется гражданином, когда в URL не указано никаких действий.
// Default route controller action
export const handler = async ( params , request , response , context ) => {
// Do some stuff
return {
// Send content and directives to the server
}
} Сервер Citizen вызывает handler() после обработки первоначального запроса и передает его 4 аргументы: объект params response параметры запроса, объекты request node.js и контекст текущего запроса.
params config | Конфигурация вашего приложения, включая любые настройки для текущего действия контроллера |
route | Подробная информация о запрошенном маршруте, таком как URL и название контроллера маршрута |
url | Любые параметры, полученные из URL |
form | Данные, собранные из сообщения |
payload | Полезной нагрузки запроса RAW |
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 как params.route.descriptor .
Аргумент context содержит любые данные или директивы, которые были сгенерированы предыдущими контроллерами в цепочке, используя их оператор return .
Чтобы вернуть результаты действия контроллера, включите оператор return с любыми данными и директивами, которые вы хотите передать гражданину.
Используя приведенные выше параметры URL, я могу получить содержимое статьи из модели и передать его обратно на сервер:
// article controller
export const handler = async ( params ) => {
// Get the article
const article = await app . models . article . get ( {
article : params . url . article ,
page : params . url . page
} )
const author = await app . models . article . getAuthor ( {
author : article . author
} )
// Any data you want available to the view should be placed in the local directive
return {
local : {
article : article ,
author : author
}
}
} Альтернативные действия могут быть запрошены с помощью параметра URL -адреса action . Например, возможно, мы хотим другое действие и представление, чтобы редактировать статью:
// 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 , как показано выше. Дополнительные объекты могут быть переданы Citizen для установки директив, которые предоставляют инструкции для сервера (см. Директивы Controller). Вы даже можете добавить свои собственные объекты в контекст и передать их от контроллера к контроллеру (больше в разделе цепочки контроллера.)
Модели являются дополнительными модулями, и их структура полностью зависит от вас. Гражданин не разговаривает с вашими моделями напрямую; Он хранит их только в 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
} Citizen использует шаблонные литералы для представления по умолчанию по умолчанию. Вы можете установить Consolidate.js и использовать любой поддерживаемый шаблонный двигатель. Просто обновите настройку конфигурации templateEngine соответственно.
В article.html вы можете ссылаться на переменные, которые вы размещаете в local объекте, переданный в оператор возврата контроллера маршрута. Гражданин также автоматически вводит свойства из объекта params в контексте вашего представления, поэтому у вас есть доступ к этим объектам в качестве локальных переменных (например, с помощью url -масштаба):
<!-- article.html -->
<!doctype html >
< html >
< body >
< main >
< h1 >
${local.article.title} — Page ${url.page}
</ h1 >
< h2 > ${local.author.name}, ${local.article.published} </ h2 >
< p >
${local.article.summary}
</ p >
< section >
${local.article.text}
</ section >
</ main >
</ body >
</ html > По умолчанию сервер отображает представление, чье имя соответствует имени контроллера. Чтобы представить другое представление, используйте директиву view в своем операторе возврата.
Все просмотры идут /app/views . Если у контроллера есть несколько представлений, вы можете организовать их в каталоге, названном в честь этого контроллера.
app/
controllers/
routes/
article.js
index.js
views/
article/
article.html // Default article controller view
edit.html
index.html // Default index controller view
Вы можете сообщить контроллеру маршрута вернуть свои локальные переменные в качестве JSON или JSON-P, установив соответствующий заголовок HTTP Accept в вашем запросе, позволяя одному и тому же ресурсу обслуживать как полное представление HTML, так и JSON для запросов AJAX и RESTFUL API.
Действие контроллера маршрута handler() .
{
"article" : {
"title" : " My Clever Article Title " ,
"summary" : " Am I not terribly clever? " ,
"text" : " This is my article text. "
},
"author" : {
"name" : " John Smith " ,
"email" : " [email protected] "
}
} Все, что вы добавили в оператор возврата контроллера, будет возвращен local объект.
Для JSONP используйте callback в URL:
http://www.cleverna.me/article/My-Clever-Article-Title/callback/foo
Возвращает:
foo ( {
"article" : {
"title" : "My Clever Article Title" ,
"summary" : "Am I not terribly clever?" ,
"text" : "This is my article text."
} ,
"author" : {
"name" : "John Smith" ,
"email" : "[email protected]"
}
} ) ; Чтобы заставить конкретный тип контента для данного запроса, установите response.contentType в контроллере маршрута на ваш желаемый вывод:
export const handler = async ( params , request , response ) => {
// Every request will receive a JSON response regardless of the Accept header
response . contentType = 'application/json'
}Вы можете вызвать глобальный тип ответа во всех запросах в рамках крючка событий.
Помощники являются дополнительными модулями утилиты, и их структура полностью зависит от вас. Они хранятся в app.helpers для вашего удобства. Вы также можете импортировать их вручную в свои контроллеры и модели, если вы предпочитаете.
Следующая функция, когда она будет размещена в app/helpers/validate.js , будет доступна в вашем приложении через app.helpers.validate.email() :
// app.helpers.validate.email()
export const email = ( address ) => {
const emailRegex = new RegExp ( / [a-z0-9!##$%&''*+/=?^_`{|}~-]+(?:.[a-z0-9!##$%&''*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])? / i )
return emailRegex . test ( address )
} Citizen хранит все модули в app не только для удобного поиска, но и для поддержки замены горячих модулей (HMR). Когда вы сохраняете изменения в любом модуле или просмотре в режиме разработки, Citizen очищает существующий импорт модуля и повторения, которые модуль в режиме реального времени.
Вы увидите журнал консоли, отмечающий пораженный файл, и ваше приложение будет продолжать работать. Не нужно перезагрузить.
Гражданин делает все возможное, чтобы грациозно обработать ошибки, не выходя из процесса. Следующее действие контроллера принесет ошибку, но сервер ответит 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, а маршрут подает ошибку, Citizen вернет ошибку в формате JSON.
Скелет приложения, созданный утилитой каркаса, включает в себя необязательные шаблоны просмотра ошибок для общих ошибок клиента и сервера, но вы можете создавать шаблоны для любого кода ошибок HTTP.
Метод обработки ошибок по умолчанию Citizen - это capture , который пытается грациозное выздоровление. Если вы предпочитаете выйти из процесса после ошибки, измените config.citizen.errors на exit .
// config file: exit the process after an error
{
"citizen" : {
"errors" : "exit"
}
}После того, как обработчик ошибок приложения Citizen выйдет из процесса.
Чтобы создать пользовательские представления ошибок для ошибок сервера, создайте каталог с названием /app/views/error и заполните его шаблонами, названными после кода ответа HTTP или кода ошибки узла.
app/
views/
error/
500.html // Displays any 500-level error
404.html // Displays 404 errors specifically
ENOENT.html // Displays bad file read operations
error.html // Displays any error without its own template
В дополнение к просмотру данных, оператор возврата контроллера маршрута может также передавать директивы для визуализации альтернативных представлений, установить файлы cookie и переменные сеанса, инициировать перенаправления, вызов и рендеринг, включают в себя действия/представления контроллера маршрута кэша (или целые запросы) и передайте запрос другому контроллеру для дальнейшей обработки.
По умолчанию сервер отображает представление, чье имя соответствует имени контроллера. Чтобы представить другое представление, используйте директиву view в своем операторе возврата:
// article controller
export const edit = async ( params ) => {
const article = await app . models . article . get ( {
article : params . url . article ,
page : params . url . page
} )
return {
local : article ,
// This tells the server to render app/views/article/edit.html
view : 'edit'
}
} Вы устанавливаете файлы cookie, возвращая объект cookie в действии контроллера.
export const handler = async ( params ) => {
return {
cookie : {
// Cookie shorthand sets a cookie called username using the default cookie properties
username : params . form . username ,
// Sets a cookie called last_active that expires in 20 minutes
last_active : {
value : new Date ( ) . toISOString ( ) ,
expires : 20
}
}
}
}Вот пример полных настроек по умолчанию объекта cookie:
myCookie = {
value : 'myValue' ,
// Valid expiration options are:
// 'now' - deletes an existing cookie
// 'never' - current time plus 30 years, so effectively never
// 'session' - expires at the end of the browser session (default)
// [time in minutes] - expires this many minutes from now
expires : 'session' ,
path : '/' ,
// citizen's cookies are accessible via HTTP/HTTPS only by default. To access a
// cookie via JavaScript, set this to false.
httpOnly : true ,
// Cookies are insecure when set over HTTP and secure when set over HTTPS.
// You can override that behavior globally with the https.secureCookies setting
// in your config or on a case-by-case basis with this setting.
secure : false
} После того, как файлы cookie установлены на клиенте, они доступны в params.cookie в контроллерах и просто cookie в виде:
<!doctype html >
< html >
< body >
< section >
Welcome, ${cookie.username}.
</ section >
</ body >
</ html > Переменные cookie, которые вы устанавливаете в своем контроллере, не сразу доступны в рамках params.cookie . Citizen должен получить контекст от контроллера и сначала отправить ответ на клиента, поэтому используйте локальный экземпляр переменной, если вам необходимо получить к нему доступ во время того же запроса.
Все файлы cookie, установленные Citizen, начинают с префикса ctzn_ , чтобы избежать столкновений. Не начинайте свои имена cookie с ctzn_ , и у вас не должно быть проблем.
Если вы используете Citizen за прокси, таким как Nginx или Apache, убедитесь, что в конфигурации вашего сервера у вас есть Forwarded HTTP, поэтому обработка Citizen Secure Cookie работала правильно.
Вот пример того, как вы можете настроить это в 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 , started , expires и timer . Идентификатор сеанса также отправляется клиенту в качестве файла cookie под названием ctzn_session_id .
Установка переменных сеанса в значительной степени такой же, как и настройка переменных cookie:
return {
session : {
username : 'Danny' ,
nickname : 'Doc'
}
} Как и файлы cookie, переменные сеанса, которые вы только что назначили, недоступны во время того же запроса в рамках params.session , поэтому используйте локальный экземпляр, если вам нужно сразу же получить доступ к этим данным.
Срок действия сеансов истекает на основе свойства конфигурации sessions.lifespan , который представляет длину сеанса за считанные минуты. По умолчанию 20 минут. timer сбрасывается с каждым запросом от пользователя. Когда timer заканчивается, сеанс удаляется. Любые запросы клиентов после этого времени генерируют новый идентификатор сеанса и отправят новую идентификацию сеанса Cookie клиенту.
Чтобы насильственно ясно и истекать срока действия сеанса текущего пользователя:
return {
session : {
expires : 'now'
}
} Все переменные сессии, установленные Citizen, начинают с префикса ctzn_ , чтобы избежать столкновений. Не запускайте имена переменных с сеансом с ctzn_ , и у вас не должно быть проблем.
Вы можете передать инструкции перенаправления на сервер, который будет инициирован после обработки действия контроллера.
Объект redirect берет строку URL в своей сокращении версии, или три параметра: statusCode , url и refresh . Если вы не предоставите код статуса, Citizen использует 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 отправит визуализированное представление клиенту, потому что перенаправление происходит на стороне клиента.
Используя разрывы заголовка местоположения (на мой взгляд) заголовок реферата, потому что реферат в конечном итоге является не ресурсом, который инициировал перенаправление, а ресурс перед страницей, которая его инициировала. Чтобы обойти эту проблему, Citizen хранит переменную сеанса, называемую ctzn_referer , которая содержит URL -адрес ресурса, который инициировал перенаправление, которое вы можете использовать для правильного перенаправления пользователей. Например, если неавтотимированный пользователь пытается получить доступ к безопасной странице, и вы перенаправляете их в форму входа, адрес безопасной страницы будет сохранен в ctzn_referer , чтобы вы могли отправить их туда вместо предыдущей страницы.
Если вы не включили сессии, Citizen возвращается к созданию файла cookie с именем ctzn_referer .
Если вы используете Citizen за прокси, например, 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;
}
Вы можете установить заголовки HTTP, используя директиву header :
return {
header : {
'Cache-Control' : 'max-age=86400' ,
'Date' : new Date ( ) . toISOString ( )
}
} Вы также можете установить заголовки непосредственно, используя метод Node's response.setHeader() , но использование директивы Citizen's header сохраняет эти заголовки в кэше запроса, поэтому они будут применяться всякий раз, когда это действие контроллера будет вытягивается из кэша.
Citizen позволяет вам использовать полные шаблоны 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 >Вероятно, это имеет смысл использовать для раздела и заголовка заголовка, потому что вы используете этот код повсюду, но вместо простых частичных, вы можете создать Citizen Incred. Раздел головки может использовать свою собственную модель для заполнения метаданных, и, поскольку заголовок отличается для аутентифицированных пользователей, давайте вытащу эту логику из вида и поместим ее в контроллер заголовка. Мне нравится следить за соглашением о начале частичных с подчеркиванием, но это зависит от вас:
app/
controllers/
routes/
_head.js
_header.js
article.js
models/
_head.js
article.js
views/
_head.html
_header/
_header.html
_header-authenticated.html // A different header for logged in users
article.html
Когда контроллер статьи уволен, он должен сообщить гражданину, который включает в себя его потребности. Мы делаем это с директивой include :
// article controller
export const handler = async ( params ) => {
// Get the article
const article = await app . models . article . get ( {
article : params . url . article ,
page : params . url . page
} )
return {
local : {
article : article
} ,
include : {
// Include shorthand is a string containing the pathname to the desired route controller
_head : '/_head/action/article' ,
// Long-form include notation can explicitly define a route controller, action, and view
_header : {
controller : '_header' ,
// If the username cookie exists, use the authenticated action. If not, use the default action.
action : params . cookie . username ? 'authenticated' : 'handler'
}
}
}
} Гражданин включает в себя модели, которые имеют те же требования, что и регулярные модели, в том числе контроллер с публичными действиями. include выше директива говорит гражданину вызвать контроллеры _head и _header , передать им те же аргументы, которые были переданы контроллеру article (параметры, запрос, ответ, контекст), представляют свои соответствующие представления и добавляют полученные представления в контекст представления.
Вот как может выглядеть контроллер нашего головного секции:
// _head controller
export const article = async ( params ) => {
let metaData = await app . models . _head ( { article : params . url . article } )
return {
local : {
metaData : metaData
}
}
}И представление в разделе головы:
< head >
< title > ${local.metaData.title} </ title >
< meta name =" description " content =" ${local.metaData.description} " >
< meta name =" keywords " content =" ${local.metaData.keywords} " >
< link rel =" stylesheet " type =" text/css " href =" site.css " >
</ head >Вот как может выглядеть наш контроллер заголовка:
// _header controller
// No need for a return statement, and no need to specify the view
// because handler() renders the default view.
//
// Every route controller needs at least one action, even if it's empty.
export const handler = ( ) => { }
export const authenticated = ( ) => {
return {
view : '_header-authenticated'
}
}И виды заголовка:
<!-- /views/_header/_header.html -->
< header >
< a href =" /login " > Login </ a >
</ header > <!-- /views/_header/_header-authenticated.html -->
< header >
< p > Welcome, ${cookie.username} </ p >
</ header > Включенные, которые хранятся в include :
<!-- /views/article.html -->
<!doctype html >
< html >
${include._head}
< body >
${include._header}
< main >
< h1 > ${local.title} — Page ${url.page} </ h1 >
< p > ${local.summary} </ p >
< section > ${local.text} </ section >
</ main >
</ body >
</ html >Гражданин включает в себя автономные и доставляемые в звонок-контроллер в качестве полностью стимулированного взгляда. В то время как они получают те же данные (параметры URL, входы формы, контекст запроса и т. Д.) В качестве вызывающего контроллер, данные, сгенерированные внутри, не переданы обратно в вызывающий абонент.
Образец, предназначенный для использования в качестве включения, можно получить через HTTP, как и любой другой контроллер маршрута. Вы можете попросить контроллера _header так же, как и в качестве ответа, и получить кусок HTML или JSON:
http://cleverna.me/_header
Это отлично подходит для обработки первого сервера запроса, а затем для обновления контента с помощью клиентской библиотеки.
Гражданин включает в себя предоставление богатых функциональности, но они имеют ограничения и могут быть излишними в определенных ситуациях.
Citizen позволяет вам объединить несколько контроллеров маршрутов в серии из одного запроса, используя next директиву. Запрашиваемый контроллер передает свои данные и визуализировал представление в последующий контроллер, добавляя свои собственные данные и предоставляя собственное представление.
Вы можете собрать столько контроллеров маршрутов вместе в одном запросе, сколько хотите. Каждый контроллер маршрута будет иметь свои данные и просмотр вывода, хранящихся в объекте params.route.chain .
// The index controller accepts the initial request and hands off execution to the article controller
export const handler = async ( params ) => {
let user = await app . models . user . getUser ( { userID : params . url . userID } )
return {
local : {
user : user
} ,
// Shorthand for next is a string containing the pathname to the route controller.
// URL paramaters in this route will be parsed and handed to the next controller.
next : '/article/My-Article/id/5'
// Or, you can be explicit, but without parameters
next : {
// Pass this request to app/controllers/routes/article.js
controller : 'article' ,
// Specifying the action is optional. The next controller will use its default action, handler(), unless you specify a different action here.
action : 'handler' ,
// Specifying the view is optional. The next controller will use its default view unless you tell it to use a different one.
view : 'article'
}
// You can also pass custom directives and data.
doSomething: true
}
}Каждый контроллер в цепи имеет доступ к контексту и представлениям предыдущего контроллера. Последний контроллер в цепочке обеспечивает окончательное представление. 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.