ЗАВАНТАЖИТИ ВИХІДНІ КОДИ

У даній статті ми створимо повноцінний сайт з контрольною панеллю для управління контентом. Як ви можете здогадатися, у підсумкової версії проекту буде дуже багато різних файлів. Я писав даний мануал крок за кроком, паралельно займаючись розробкою. Весь вихідний код доступний на GitHub, і я рекомендую вам користуватися ним в процесі роботи.

Введення

Express це один з кращих фреймворків для Node. У нього відмінна підтримка з боку розробників і безліч можливостей. За Express є багато інформації в інтернеті, що розкриває ази роботи з ним. У цій статті я хочу копнути трохи глибше основ і поділитися своїм досвідом створення повноцінного веб-сайту.

В цілому, мова піде не тільки про Express – в роботі використовуються і інші хороші інструменти, що знаходяться в арсеналі Node-розробника.

Передбачається, що ви знайомі з Node.js, маєте встановлену копію на своєму комп’ютері і певний досвід розробки.

Серцем Express є Connect. Це сполучний фреймворк, що містить багато корисних речей. Якщо ви не розумієте що з себе представляє сполучний фреймворк (middleware framework), то погляньте на невеликий приклад:

var connect = require(‘connect’),
http = require(‘http’);
var app = connect()
.use(function(req, res, next) {
console.log(«Це мій перший Middleware Framework»);
next();
})
.use(function(req, res, next) {
console.log(«Це мій другий Middleware Framework»);
next();
})
.use(function(req, res, next) {
console.log(«Кінець»);
res.end(«Hello World!»);
});
http.createServer(app).listen(3000);

Middleware (проміжне) це функція, яка приймає запити від об’єктів і відповідає на них. Кожне middleware може відповісти, використовуючи відповідний об’єкт або передати управління наступного middleware, використовуючи функцію next().

У прикладі вище, при видаленні виклику методу next() у другому middleware, рядок «Hello World!» ніколи не буде передана браузеру. Так, в загальних рисах, працює Express.

У складі фреймворку є кілька визначених middleware, що, безсумнівно, економить час. Наприклад, парсер Body, підтримує типи application/json, application/x-www-form-urlencoded і multipart/form-data, який обробляє тіло запиту. Чи парсер Cookie, обробний заголовки cookie і populatesreq.cookies з допомогою об’єкта, асоційованого з ім’ям cookie.

Express доповнює Connect і додає в нього нову функціональність, що робить розробку більш зручною, наприклад, функцію логіки маршрутизації. Нижче наведен приклад управління запитом GET:

app.get(‘/hello.txt’, function(req, res){
var body = ‘Hello World!’;
res.setHeader(‘Content-Type’, ‘text/plain’);
res.setHeader(‘Content-Length’, body.length);
res.end(body);
});

Налаштування

Є два способи налаштування Express. Перший – розміщення файлу package.json і запуск установки через пакетний менеджер npm.

{
«name»: «MyWebSite»,
«description»: «My website»,
«version»: «0.0.1»,
«dependencies»: {
«express»: «3.x»
}
}

Код фреймворку буде розміщений папці node_modules, і ви зможете створити копію. Однак я віддаю перевагу альтернативний варіант – використання командного рядка. Для цього потрібно запустити команду npm install -g express. Після чого, Express буде готовий до роботи. Для перевірки запустіть:

express —sessions —css less —hogan app

Після чого Express створить скелет попередньо настроєний програми. Ось список керуючих команд для команди express:

Приклад використання: express [список параметрів]

Параметри:

-h, —help виведення довідки по параметрам;
-V, —version висновок номера версії;
-s, —sessions активація підтримки сесій;
-e, —ejs активація підтримки движка ejs (за замовчуванням для Jade);
-J, —jshtml активація підтримки движка jshtml (за замовчуванням для Jade);
-H, —hogan активація підтримки движка hogan.js;
-c, —css активація підтримки стилів (Less|Stylus) (за замовчуванням для Plain CSS);
-f, —force примусові непусті директорії.

Як бачите, команд не так вже й багато, але цього вистачає. Зазвичай я використовую CSS-препроцессоры less і hogan для шаблонізації.

У нашому прикладі, нам також знадобиться підтримка сесій, в чому допоможе аргумент —sessions. Коли команда, наведена в лістингу вище, виконається, структура папок нашого проекту буде такою:

/public
/images
/javascript
/stylesheets
/routes
/index.js
/user.js
/views
/index.hjs
/app.js
/package.json

Якщо ви відкриєте файл package.json, то побачите, що всі необхідні залежності були додані, хоча вони ще не були встановлені. Щоб зробити це, просто запустіть установку через npm, і з’явиться папка node_modules.

Я усвідомив, що метод, описаний вище, не завжди доречний. Можливо, ви захочете помістити свої обробники маршрутів в іншу папку або потрібно буде зробити щось ще. Але, як ви зрозумієте з наступних глав, я внесу зміни в згенеровані структури, що досить легко зробити. Так що просто думайте про команду express як про шаблонизаторе.

FastDelivery

Для цієї статті, я розробив простий сайт вигаданої компанії FastDelivery. Ось скріншот, демонструє закінчений дизайн:

В кінці статті, у нас вийде повноцінне веб-додаток, з контрольною панеллю. Основна ідея полягає в тому, щоб кожній частиною нашого сайту можна було управляти окремо.

Шаблон був створений в Photoshop і оформлений у файлах CSS (less) і HTML (hogan). Я не буду показувати процес створення шаблону, так як це не відноситься до теми нашої статті. Після створення шаблону, структура файлів нашого проекту повинна бути наступною:

/public
/images (кілька зображень, експортованих з Photoshop)
/javascript
/stylesheets
/home.less
/inner.less
/style.css
/style.less (імпорт home.less та inner.less)
/routes
/index.js
/views
/index.hjs (домашня сторінка)
/inner.hjs шаблон всіх сторінок сайту)
/app.js
/package.json

Ми збираємося адмініструвати такі елементи сайту:

  • Головна (банер у центрі – заголовок і текст);
  • Блог (додавання, видалення і редагування статей);
  • Послуги;
  • Кар’єра;
  • Контакти.

Конфігурація

До написання коду нам потрібно зробити кілька речей. Уявімо, що наш маленький сайт повинен бути розгорнутий в трьох різних місцях – на локальному, тестовому та виробничому серверах. Звичайно, для кожного з них налаштування будуть відрізнятися і потрібно реалізувати досить гнучкий механізм.

Як відомо, кожен скрипт node.js запускається у вигляді консольної програми. Тому ми легко можемо вказати аргументи, які будуть визначати поточну середу. Я оформив цей код у вигляді окремого модуля, щоб пізніше протестувати. Ось вміст файлу /config/index.js:

var config = {
local: {
mode: ‘local’,
port: 3000
},
staging: {
mode: ‘staging’,
port: 4000
},
production: {
mode: ‘production’,
port: 5000
}
}
module.exports = function(mode) {
return config[mode || process.argv[2] || ‘local’] || config.local;
}

Поки що тут у нас всього два параметри – mode і port. Як можна здогадатися, наш додаток використовує різні порти для різних серверів. З цієї причини, нам потрібно змінити точку входу на сайт у файлі app.js.


var config = require(‘./config’)();

http.createServer(app).listen(config.port, function(){
console.log(‘Express server listening on port’ + config.port);
});

Для перемикання між конфігураціями, просто додайте тип сервера в кінець команди. Наприклад:

node app.js staging

Ось що відбудеться:

Express server listening on port 4000

Тепер, всі наші налаштування знаходяться в одному місці і ними легко управляти.

Тестування

Я великий прихильник підходу test-driven development (розробка через тестування). Я спробую розповісти про всіх основних класах, що використовуються в даній статті. Зрозуміло, тестування абсолютно все зробить статтю неймовірно великий, і з цієї причини я цього робити не буду.

Але в цілому, у вас повинно скластися розуміння, як це робиться при створенні власних програм. Одним з моїх найулюбленіших фреймворків для тестування є jasmine. Він також доступний для установки через npm:

npm install -g jasmine-node

Давайте створимо папку, в якій будуть розташовуватися наші тести. Перше, що ми збираємося перевірити, це наш скрипт з конфігурацією. Spec-файли повинні закінчуватися на .spec.js, тому ми назвемо наш config.spec.js.

describe(«Конфігурація», function() {
it(«локального сервера», function(next) {
var config = require(‘../config’)();
expect(config.mode).toBe(‘local’);
next();
});
it(«тестового сервера», function(next) {
var config = require(‘../config’)(‘staging’);
expect(config.mode).toBe(‘staging’);
next();
});
it(«виробничого сервера», function(next) {
var config = require(‘../config’)(‘production’);
expect(config.mode).toBe(‘production’);
next();
});
});

Запускаємо jasmine-node ./tests і бачимо наступну картину:

Finished in seconds 0.008
3 tests, 6 assertions, 0 failures, 0 skipped

Отже, я спочатку створив конфігурацію, а потім провів її тест. Це не занадто близько до підходу «розробка через тестування», але далі, я буду більш суворо його дотримуватися.

Дуже рекомендується приділити достатню час тестування. Немає нічого краще, ніж добре протестований додаток.

Пару років тому я зрозумів дещо важливе, що може допомогти вам створювати більш досконалі програми. Кожен раз, коли ви починаєте писати новий клас, модуль або будь-якої іншої логічно закінчений шматок коду, запитайте себе: Як я можу його протестувати?

Відповідь на це питання допоможе вам писати код більш ефективно, створювати більш якісні API і грамотно розташовувати частини програми за окремими блоками. Ви не зможете написати тест для коду, заплутаного як спагетті. Наприклад, в конфігураційному файлі вище (/config/index.js), я додав можливість передавати режим конструктор модуля.

Ви, можливо, будете здивовані, чому я зробив так, коли спочатку передбачалося отримувати режим через аргументи командного рядка. Все просто — мені треба провести тестування.

Уявімо, що місяцем пізніше мені потрібно буде перевірити, що-небудь у конфігурації для виробничого сервера, але скрипт node запускається з параметрами командного рядка. Я навряд чи зможу згадати всі налаштування. Тому, відхід від первісної ідеї допоможе в майбутньому уникнути проблем.

База даних

Після того, як ми створили динамічний сайт, необхідно зберегти дані в базі. Для прикладу в даній статті, я буду використовувати базу даних mongodb. Mongo це документо-орієнтована СУБД без підтримки SQL. Інструкції по її установці можна знайти тут.

Так як я використовую Windows, то мені знадобляться інструкції по установці Windows. Після закінчення установки, запустіть демон MongoDB, який за замовчуванням слухає порт 27017. Теоретично, ми можемо підключитися до цього порту і взаємодіяти з сервером mongodb.

Щоб зробити це з node-скрипта, нам знадобиться модуль/драйвер mongodb. Якщо ви скачаєте вихідні файли до цієї статті, то цей модуль в них вже включений у файлі package.json. В іншому випадку, просто додайте «mongodb»: «1.3.10» в список залежностей і запустіть установку через npm.

Далі, ми напишемо тест, що перевіряє запущений сервер mongodb, який буде розташовуватися в файлі ./tests/mongodb.spec.js:

describe(«MongoDB», function() {
it(«сервер запущений», function(next) {
var MongoClient = require(‘mongodb’).MongoClient;
MongoClient.connect(‘mongodb://127.0.0.1:27017/fastdelivery’, function(err, db) {
expect(err).toBe(null);
next();
});
});
});

Callback-виклик в методі .connect клієнта mongodb, посилає об’єкт db. Ми будемо використовувати його пізніше для управління нашими даними. Це означає, що ми повинні отримувати доступ до цих даних всередині нашої моделі.

Створювати новий об’єкт MongoClient кожен раз, коли нам потрібно зробити запит до бази даних це не найкраща ідея. Ось чому я перемістив запуск сервера Express callback-виклик всередину функції connect:

MongoClient.connect(‘mongodb://127.0.0.1:27017/fastdelivery’, function(err, db) {
if(err) {
console.log(‘Вибачте, але сервер mongo db не запущений.’);
} else {
var attachDB = function(req, res, next) {
req.db = db;
next();
};
http.createServer(app).listen(config.port, function(){
console.log(‘Сервер Express слухає порт’ + config.port);
});
}
});

Навіть краще, що замість параметрів командного рядка ми використовували конфігураційний файл. Ми помістили туди ім’я хоста і номер порту mongodb, а потім змінили URL-адресу в функції connect на:

‘mongodb://’ + config.mongo.host + ‘:’ + config.mongo.port + ‘/fastdelivery’

Зверніть особливу увагу на middleware під назвою attachDB, яке я додав відразу після виклику функції http.createServer.

Спасибі вибору на користь конфігураційного файлу, завдяки якому ми можемо заповнити властивість .db для запитуваного об’єкта. Хороша новина в тому, що ми можемо приєднати кілька функцій при визначенні маршруту.

Наприклад:

app.get(‘/’, attachDB, function(req, res, next) {

})

Цим кодом, Express заздалегідь викликає функцію attachDB, щоб запустити наш обробник маршруту. Після того, як це сталося, об’єкт request отримає значення властивості .db, яке ми зможемо використовувати для доступу до бази даних.

MVC

Швидше за все, ви знайомі з MVC. Завдання полягає у застосуванні цієї схеми до Express. Більше чи менше, це питання інтерпретації. У наступних декількох розділах, я створю модулі, які будуть взаємодіяти за схемою: модель-представлення-контролер.

Модель (Model)

Модель управляє даними в нашому додатку. Вона повинна мати доступ до об’єкта db, який повертається MongoClient. Наша модель також повинна мати метод для розширення цього об’єкта, тому що, можливо, ми захочемо створити різні типи моделей.

Наприклад, ми можемо створити модель BlogModel або ContactsModel. Тому потрібно створити новий spec-файл: /tests/base.model.spec.jsдля тестування цих двох майбутніх моделей. Пам’ятайте, що визначаючи цей функціонал ДО початку реалізації у вигляді коду, ми гарантуємо, що наш модуль буде робити тільки те, що від нього очікується.

var Model = require(«../models/Base»),
dbMockup = {};
describe(«Моделі», function() {
it(«повинні створювати нові моделі», function(next) {
var model = new Model(dbMockup);
expect(model.db).toBeDefined();
expect(model.extend).toBeDefined();
next();
});
it(«бути расширяемыми», function(next) {
var model = new Model(dbMockup);
var OtherTypeOfModel = model.extend({
myCustomModelMethod: function() { }
});
var model2 = new OtherTypeOfModel(dbMockup);
expect(model2.db).toBeDefined();
expect(model2.myCustomModelMethod).toBeDefined();
next();
})
});

Замість реального об’єкта db, я вирішив передати об’єкт mockup. Це зроблено тому, що пізніше, я, можливо, захочу зробити специфічний тест, в залежності від інформації, що надходить з бази даних. Буде простіше визначити ці дані вручну.

Реалізація розширеного методу трохи більш хитра, тому що нам потрібно змінити прототип module.exports, але зберегти оригінальний конструктор. Завдяки тому, що ми раніше написали гарний тест, який підтверджує працездатність нашого коду. Код, представлений вище, буде виглядати так:

module.exports = function(db) {
this.db = db;
};
module.exports.prototype = {
extend: function(properties) {
var Child = module.exports;
Child.prototype = module.exports.prototype;
for(var key in properties) {
Child.prototype[key] = properties[key];
}
return Child;
},
setDB: function(db) {
this.db = db;
},
collection: function() {
if(this._collection) return this._collection;
return this._collection = this.db.collection(‘fastdelivery-content’);
}
}

Ось два наших helper-методу: перший ініціалізує об’єкт db, а другий отримує collection з бази даних.

Вигляд (View)

Вид відображає інформацію на екрані. По суті, це вид клас, який посилає відповідь браузеру. Express надає зручний спосіб зробити це:

res.render(‘index’, { title: ‘Express’ });

Об’єкт response є обгорткою (wrapper), яка має хороший API, що робить наше життя простіше. Однак я волію створити модуль, який буде инкапсулировать даний функціонал. Змінимо стандартну папку для видів на templates і створимо новий вид, який буде базовим класом (base class view).

Це маленька зміна тягне за собою ще одне — нам треба повідомити Express про те, що файли нашого шаблону тепер розміщені в іншій папці:

app.set(‘views’, __dirname + ‘/templates’);

Отже, для початку я визначу те, що мені потрібно, напишу тест, після чого, створю реалізацію. Нам потрібен модуль, який повинен задовольняти наступним вимогам:

  • Його конструктор повинен отримувати об’єкт response і ім’я шаблону;
  • Він повинен мати метод render, який виводить об’єкт data;
  • Він повинен бути розширюваним.

Ви здивовані тим, що я розширив клас View. Чи Не простіше просто викликати метод response.render? На практиці виникають випадки, коли вам потрібно послати інший заголовок або певним чином маніпулювати об’єктом response. Наприклад, є такі дані JSON:

var data = {«developer»: «Krasimir Tsonev»};
response.contentType(‘application/json’);
response.send(JSON.stringify(data));

Замість того, щоб робити це кожен раз, набагато простіше мати класи HTMLView і JSONView. Або навіть клас XMLView для відсилання XML-даних браузеру. Особливо це корисно, коли ви хочете створити великий сайт – у вас є шаблони і вам не потрібно багато разів копіювати-вставити один і той же код.

Ось spec для /views/Base.js:

var View = require(«../views/Base»);
describe(«Base view», function() {
it(«створює і відображає новий вид», function(next) {
var responseMockup = {
render: function(template, data) {
expect(data.myProperty).toBe(‘value’);
expect(template).toBe(‘template-file’);
next();
}
}
var v = new View(responseMockup, ‘template-file’);
v.render({myProperty: ‘value’});
});
it(«повинна бути розширюваної», function(next) {
var v = new View();
var OtherView = v.extend({
render: function(data) {
expect(data.prop).toBe(‘yes’);
next();
}
});
var otherViewInstance = new OtherView();
expect(otherViewInstance.render).toBeDefined();
otherViewInstance.render({prop: ‘yes’});
});
});

Щоб протестувати висновок, мені довелося створити об’єкт mockup. В даному випадку, я створив об’єкт, який імітує Express’івський об’єкт response. У другій частині тесту, я створив інший клас View, який наслідує модель Base і застосовує кастомный метод render. Ось це клас — /views/Base.js.

module.exports = function(response, template) {
this.response = response;
this.template = template;
};
module.exports.prototype = {
extend: function(properties) {
var Child = module.exports;
Child.prototype = module.exports.prototype;
for(var key in properties) {
Child.prototype[key] = properties[key];
}
return Child;
},
render: function(data) {
if(this.response && this.template) {
this.response.render(this.template, data);
}
}
}

Тепер у нас є три spec’а в папці tests і, якщо ми запустимо команду jasmine-node ./tests, результат буде наступним:

Finished in seconds 0.009
7 tests, 18 assertions, 0 failures, 0 skipped

Контролер (Controller)

Пам’ятайте маршрути (routes) і як ми їх визначили?

app.get(‘/’, routes.index);

Символ ‘/’, у прикладі вище, це і є контролер. Це middleware-функція, яка приймає request, response і next.

exports.index = function(req, res, next) {
res.render(‘index’, { title: ‘Express’ });
};

У наведеному вище прикладі показано, як повинен виглядати ваш контролер, в контексті Express. Команда express створює папку з ім’ям routes, але в нашому випадку, краще назвати її controllers. Тому я перейменував її таким чином, щоб відобразити використовується нами схему MVC.

Так створюване нами програма є досить серйозним, буде мудрим рішенням створити клас base, який потім може бути розширений. Якщо нам коли-небудь знадобитися передати деяку функціональність іншим контролерам, то цей клас послужить нам велику користь. І знову, я спочатку напишу тест, так що давайте визначимося, що нам потрібно від класу:

  • Він повинен мати метод nextend, який приймає об’єкт і повертає новий дочірній примірник;
  • Дочірній примірник повинен мати метод run, який є старою middleware-функцією;
  • Клас повинен містити в собі властивість name, яке ідентифікує контролер;
  • Ми повинні мати можливість створювати незалежні об’єкти, засновані на цьому класі.

Не такі вже великі можливості, але адже ми зможемо додати нову функціональність пізніше. Тест буде виглядати наступним чином:

var BaseController = require(«../controllers/Base»);
describe(«Base controller», function() {
it(«повинен мати метод extend, який повертає дочірній примірник», function(next) {
expect(BaseController.extend).toBeDefined();
var child = BaseController.extend({ name: «my child controller» });
expect(child.run).toBeDefined();
expect(child.name).toBe(«my child controller»);
next();
});
it(«повинен вміти створювати різні дочірні екземпляри», function(next) {
var childA = BaseController.extend({ name: «A child», customProperty: ‘value’ });
var childB = BaseController.extend({ name: «child B» });
expect(childA.name).not.toBe(childB.name);
expect(childB.customProperty).not.toBeDefined();
next();
});
});

А от реалізація /controllers/Base.js:

var _ = require(«underscore»);
module.exports = {
name: «base»,
extend: function(child) {
return _.extend({}, this, child);
},
run: function(req, res, next) {
}
}

Зрозуміло, кожний дочірній клас повинен визначати власний метод run, який має власну логіку.

Сайт FastDelivery

Чудово, тепер у нас є достатній набір класів для реалізації архітектури MVC. Також ми створили тест до кожного модуля. Ми готові продовжити створення сайту вигаданої компанії FastDelivery. Уявімо, що сайт розділений на дві частини – лицьова (front-end) і адміністративна (back-end).

Лицьова буде використовуватися для відображення користувачам інформації, наявної в базі даних. Адміністративна панель ж буде використовуватися для управління цими даними. Почнемо з адміністративної (контрольної) панелі.

Контрольна панель

Для початку, давайте створимо простий контролер, який буде обслуговувати адміністративну сторінку і перебувати у файлі ./controllers/Admin.js:

var BaseController = require(«./Base»),
View = require(«../views/Base»);
module.exports = BaseController.extend({
name: «Admin»,
run: function(req, res, next) {
var v = new View(res, ‘admin’);
v.render({
title: ‘Адміністративна панель’,
content: ‘Ласкаво просимо в адміністративну панель’
});
}
});

Використовуючи заздалегідь написані базові класи для контролерів і видів, ми легко можемо створити точку входу в адміністративну панель. Клас View приймає ім’я файлу шаблону. Згідно з кодом, наведеному вище, файл повинен бути названий admin.hjs розташований у папці /templates. Його вміст має бути наступним:

{{ title }}

{{ content }}

Щоб стаття не збільшувалася в обсязі, я не буду викладати кожен окремий шаблон виду. Ви можете переглянути вихідний код на GitHub.

Щоб зробити контролер видимим, нам потрібно додати в нього маршрут у файлі app.js:

var Admin = require(‘./controllers/Admin’);

var attachDB = function(req, res, next) {
req.db = db;
next();
};

app.all(‘/admin*’, attachDB, function(req, res, next) {
Admin.run(req, res, next);
});

Зауважте, що ми не посилаємо метод Admin.run безпосередньо в middleware, щоб не порушити контекст. Якщо ми зробимо так:

app.all(‘/admin*’, Admin.run);

Те слово this в адміністративному модулі буде вести в інше місце.

Захист адміністративної панелі

Кожна сторінка, що починається з /admin повинна бути захищена. Для цього, нам потрібно використовувати middleware, вбудоване в Express, під назвою Sessions. Цей інструмент просто прикріплює об’єкт до запиту названому session. Тепер нам потрібно змінити контролер нашої адміністративної панелі таким чином, щоб він робив дві речі:

  • Перевіряв, чи є сесія. Якщо ні, то відобразити форму логіна;
  • Брав дані, надіслані через форму логіна і авторизовывал користувача при збігу логіна і пароля.

Ось невелика helper-функція, яку ми можемо використовувати, щоб реалізувати це:

authorize: function(req) {
return (
req.session &&
req.session.fastdelivery &&
req.session.fastdelivery === true
) || (
req.body &&
req.body.username === this.username &&
req.body.password === this.password
);
}

У цьому лістингу спочатку йде вираз, який пробує розпізнати користувача через об’єкт session. Далі, ми перевіряємо, чи була відправлена форма. Якщо так, то дані з форми стають доступні через об’єкт request.body,який заповнюється за допомогою middleware bodyParser. Нарешті, ми перевіряємо ім’я користувача і пароль.

А тепер, реалізуємо метод контролера run, який використовує наш новий хелпер. Якщо авторизований користувач, то відображаємо адміністративну панель, інакше – панель логіна:

run: function(req, res, next) {
if(this.authorize(req)) {
req.session.fastdelivery = true;
req.session.save(function(err) {
var v = new View(res, ‘admin’);
v.render({
title: ‘Адміністративна панель’,
content: ‘Ласкаво просимо в адміністративну панель’
});
});
} else {
var v = new View(res, ‘admin-login’);
v.render({
title: ‘будь Ласка, назвіться’
});
}
}

Управління контентом

Як я пояснив вище, у нас є багато об’єктів, які потрібно адмініструвати. Щоб спростити цей процес, давайте залишимо всі дані у одній колекції. Кожна запис буде мати назву, довільний текст, картинку і властивість type.

Властивість type буде визначати власника цього запису. Наприклад, для сторінки «Контакти» буде потрібна тільки одна запис type: ‘контакти’, в той час як сторінка «Блог» вимагатиме більшу кількість записів. Тому, нам потрібно три нових сторінки для додавання, редагування та виводу записів.

Але перед тим, як ми приступимо безпосередньо до реалізації нових шаблонів, стилів і функціоналу контролера, нам потрібно написати клас нашої моделі, яка розташовується між сервером MongoDB і додатком, та надає API.

// /models/ContentModel.js
var Model = require(«./Base»),
crypto = require(«crypto»),
model = new Model();
var ContentModel = model.extend({
insert: function(data, callback) {
data.ID = crypto.randomBytes(20).toString(‘hex’);
this.collection().insert(data, {}, callback || function(){ });
},
update: function(data, callback) {
this.collection().update({ID: data.ID}, data, {}, callback || function(){ });
},
getlist: function(callback, query) {
this.collection().find(query || {}).toArray(callback);
},
remove: function(ID callback) {
this.collection().findAndModify({ID: ID}, [], {}, {remove: true}, callback);
}
});
module.exports = ContentModel;

Модель бере на себе відповідальність за створення унікального ID для кожного запису. Це потім знадобиться нам для оновлення інформації.

Якщо ми хочемо додати новий запис на сторінку «Контакти», то просто робимо наступне:

var model = new (require(«../models/ContentModel»));
model.insert({
title: «Контакти»
text: «…»,
type: «contacts»
});

Отже, у нас є відмінний API для управління даними в нашій колекції mongodb. Тепер ми готові до створення користувацького інтерфейсу для наявної функціональності. Для цього потрібно буде трохи змінити контролер адміністративної панелі.

Для спрощення завдання я вирішив поєднати список доданих записів і форму для додавання/редагування. Як ви можете побачити на скріншоті нижче, ліва частина сторінки зарезервована під списку, а права – під форму.

Створення повноцінного MVC-сайту за допомогою ExpressJS

Розташувавши всі на одній сторінці ми зможемо зосередитися на реалізації виводу та інших специфічні речі, наприклад, даних, що пересилаються в шаблон. З цієї причини, я створив кілька helper-функцій, об’єднаних між собою ось так:

var self = this;

var v = new View(res, ‘admin’);
self.del(req, function() {
self.form(req, res, function(formMarkup) {
self.list(function(listMarkup) {
v.render({
title: ‘Адміністративна панель’,
content: ‘Ласкаво просимо в адміністративну панель’,
list: listMarkup,
form: formMarkup
});
});
});
});

Наша адміністративна панель виглядає дуже незграбно, але працює саме так, як замислювалося. Перша helper-функція це метод del, який перевіряє поточні параметри GET і, якщо знаходить рядок action=delete&id=[id запису], то видаляє дані з колекції.

Друга функція називається form і відповідає в основному за відображення форми в правій частині сторінки. Вона перевіряє, відправлена форма, і оновлює/створює записи в базі даних. В кінці, метод list вибирає інформацію і готує HTML таблицю, яка буде надіслана шаблоном. Реалізацію цих трьох хелперів можна знайти тут.

У статті ж, я вирішив показати функцію, яка управляє завантаженням файлу:

handleFileUpload: function(req) {
if(!req.files || !req.files.picture || !req.files.picture.name) {
return req.body.currentPicture || «;
}
var data = fs.readFileSync(req.files.picture.path);
var fileName = req.files.picture.name;
var uid = crypto.randomBytes(10).toString(‘hex’);
var dir = __dirname + «/../public/wp-content/uploads/» + uid;
fs.mkdirSync(dir, ‘0777’);
fs.writeFileSync(dir + «/» + fileName, data);
return ‘/wp-content/uploads/’ + uid + «/» + fileName;
}

Якщо файл відправлений властивість .files об’єкта request заповнюється даними. В нашому випадку, у нас є наступний HTML-елемент:

Це означає, що ми можемо отримати доступ до надісланих даними через req.files.picture. В коді, наведеному вище, req.files.picture.path використовується, щоб отримати необроблене вміст файлу.

Пізніше, в ті ж дані записується новий каталог і в кінці повертається URL-адресу. Всі ці операції синхронні, але дуже корисно використовувати асинхронні версії readFileSync, mkdirSync і writeFileSync.

Лицьова частина (Front-End)

Найскладніша частина роботи виконана. Адміністративна панель працює і у нас є клас ContentModel, який дає доступ до інформації, збереженої в базі даних. Тепер нам потрібно реалізувати контролери фронт-енду і прив’язати їх до збереженого вмісту.

Нижче представлений контролер для домашньої сторінки — /controllers/Home.js:

module.exports = BaseController.extend({
name: «Домашня сторінка»,
content: null,
run: function(req, res, next) {
model.setDB(req.db);
var self = this;
this.getContent(function() {
var v = new View(res, ‘home’);
v.render(self.content);
})
},
getContent: function(callback) {
var self = this;
this.content = {};
model.getlist(function(err, records) {
… тут йде збереження даних в об’єкт content
model.getlist(function(err, records) {
… тут йде збереження даних в об’єкт content
callback();
}, { type: ‘blog’ });
}, { type: ‘home’ });
}
});

Домашня сторінка вимагає одного запису типу home і чотирьох типу blog. Після створення контролера, нам потрібно додати маршрут до файлу app.js:

app.all(‘/’, attachDB, function(req, res, next) {
Home.run(req, res, next);
});

І знову, ми додаємо об’єкт db до request. Це практично те ж саме, що ми робили для адміністративної панелі.

Інші сторінки для нашого фронт-енду (клієнтської частини) ідентичні: всі вони мають контролер, який витягає дані з допомогою класу моделі і визначає маршрут. Є кілька моментів, які я б хотів пояснити. Перший стосується сторінки «Блог».

Вона повинна вміти відображати не тільки всі статті, але і кожну з них окремо. Тому, ми повинні зареєструвати два маршрути:

app.all(‘/blog/:id’, attachDB, function(req, res, next) {
Blog.runArticle(req, res, next);
});
app.all(‘/blog’, attachDB, function(req, res, next) {
Blog.run(req, res, next);
});

Обидві функції використовують один і той же контролер Blog, але викликають метод run по-різному. Зверніть увагу на рядок /blog/:id. Цей маршрут буде збігатися з URL-адресами виду /blog/4e3455635b4a6f6dccfaa1e50ee71f1cde75222b, а довга хеш-функція буде доступна через req.params.id. Іншими словами, ми можемо визначити динамічні параметри.

В даному випадку, це ID запису. Після отримання цієї інформації, ми можемо створити унікальну сторінку для кожної статті.

Другим цікавим моментом є те, яким чином я створив сторінки «Послуги», «Кар’єра» і «Контакти». Ясно, що вони використовують тільки один запис з бази даних. Якщо нам потрібно створити різні контролери для кожної сторінки, то необхідно скопіювати/вставити той же код і змінити поле type.

Це оптимальний спосіб, коли є тільки один контролер, який приймає значення type в методі run. Отже, ось маршрути:

app.all(‘/services’, attachDB, function(req, res, next) {
Page.run(‘services’, req, res, next);
});
app.all(‘/careers’, attachDB, function(req, res, next) {
Page.run(‘careers’, req, res, next);
});
app.all(‘/contacts’, attachDB, function(req, res, next) {
Page.run(‘контакти’, req, res, next);
});

А ось як буде виглядати контролер:

module.exports = BaseController.extend({
name: «Page»
content: null,
run: function(type, req, res, next) {
model.setDB(req.db);
var self = this;
this.getContent(type function() {
var v = new View(res, ‘inner’);
v.render(self.content);
});
},
getContent: function(type, callback) {
var self = this;
this.content = {}
model.getlist(function(err, records) {
if(records.length > 0) {
self.content = records[0];
}
callback();
}, { type: type });
}
});

Розгортання

Процедура розгортання сайту на базі Express аналогічна, розгортання будь-якого іншого Node.js-додатки:

  • Переміщення файлів на сервер;
  • Зупинка процесу node (якщо він запущений);
  • Запуск команди npm install для установки нових залежностей;
  • Запуск node.

Треба розуміти, що Node це досить молода платформа, і не все може працювати, як очікується, але поліпшення робляться постійно. Наприклад, CLI-інструмент forever гарантує, що ваше Node.js-додаток буде запущено вічно. Це робиться командою:

forever start yourapp.js

Я використовую це на всіх серверах. Це відмінний інструмент, який вирішує безліч проблем. Якщо ви запускаєте програму з допомогою node yourapp.jsпісля несподіваного завершення її роботи, сервер впаде. Forever, просто запускає додаток в цьому випадку.

Я не системний адміністратор, але у мене є бажання поділитися своїм досвідом інтеграції node-додатків з Apache і Nginx, тому що я вважаю, що це частина робочого процесу і допомагає розвитку програмного забезпечення в принципі.

Як ви знаєте, Apache нормально працює на 80 порту, а це означає, що якщо ви перейдете за адресою http://localhost або http://localhost:80, то побачите сторінку Apache-сервера. Найчастіше, ваш node-скрипт слухає інший порт.

Тому, вам потрібно додати віртуальний хост, який буде приймати запити і направляти їх на потрібний порт. Уявімо, що я хочу розташувати створений нами сайт, на своєму локальному Apache-сервері за адресою expresscompletewebsite.dev. Для цього, насамперед, потрібно додати наш домен в файл hosts:

127.0.0.1 expresscompletewebsite.dev

Після чого, треба відредагувати файл httpd-vhosts.conf, розташований в папці з конфігураційними файлами Apache, додавши до нього:

# expresscompletewebsite.dev
ServerName expresscompletewebsite.dev
ServerAlias www.expresscompletewebsite.dev
ProxyRequests off
Order deny,allow
Allow from all
ProxyPass http://localhost:3000/
ProxyPassReverse http://localhost:3000/

Сервер все ще посилає запити на порт 80, але перенаправляє їх на порт 3000, де їх слухає node.

Налаштування Nginx простіше і, чесно кажучи, він краще підходить для Nodejs-додатків. Першим кроком все також потрібно додати наш домен в файл hosts. Після чого, просто створіть новий файл в папці /sites-enabled в директорії з встановленим Nginx. Вміст файлу має виглядати наступним чином:

server {
listen 80;
server_name expresscompletewebsite.dev
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $http_host;
}
}

Ви не зможете запустити Apache і Nginx з налаштуваннями hosts-файлів, наведеними вище, тому що вони вимагають порт 80. Також, якщо ви системний адміністратор, то, швидше за все, захочете поекспериментувати з налаштуваннями для поліпшення продуктивності. Але повторюся, я не експерт у цій галузі.

Висновок

Express-це чудовий фреймворк, який добре підходить для того, щоб почати створювати свої веб-додатки. І, як ви могли помітити, результат буде залежати від того, яким чином ви побудуєте його, і будете використовувати при створенні.

Express спрощує вирішення рутинних завдань, він надає відмінне middleware і, в той же час, залишає реалізацію відповідальних частин коду розробнику.

Вихідний код

Початкові коди для даної статті доступні на GitHub. Використовуйте їх і експериментуйте.

Ось коротка інструкція по запуску сайту:

  • Завантажте вихідні коди;
  • Перейдіть в папку app;
  • Запустіть npm install;
  • Запустіть демон mongodb;
  • Виконайте команду node app.js.

Переклад статті «Build a Complete MVC Website With ExpressJS» був підготовлений дружною командою проекту Сайтостроение від А до Я.