Ukrainian (українська мова) translation by AlexBioJS (you can also view the original English article)
Ви вже вдало створили CMS (* content management system – система керування контентом; використовують для організації автоматизованого збирання, керування та публікації інформації; ПЗ для фільтрації, публікації, обслуговування і відновлення контенту веб-сайтів і порталів), що працює на основі плоскої файлової системи (* система організації файлів, при якій вони не можуть мати однакових імен, якщо навіть знаходяться у різних каталогах), за допомогою Go. Наступний крок – реалізувати те саме за допомогою Node.js. Я покажу вам, як завантажити бібліотеки, створити та запустити сервер.
У цій CMS ми будемо використовувати ту структуру даних сайту, яку було показано у першому посібнику, «Building a CMS: Structure and Styling». Тому завантажте та витягніть архів з цією базовою структурою до порожньої папки.
Встановлюємо Node та бібліотеки для неї:
Найпростіший спосіб встановити Node.js на Mac – за допомогою Homebrew (* найпопулярніший пакетний менеджер для Mac). Якщо Homebrew у вас ще не встановлено, то можете подивитися, як це зробити, у посібнику «Homebrew Demystified: OS X’s Ultimate Package Manager».
Для встановлення Node.js за допомогою Homebrew виконайте наступну команду у командному рядку:
brew install node
Після її виконання на вашому Mac стануть доступні всі команди з node та npm. Для встановлення Node.js на решту платформ дотримуйтесь інструкцій, які наведено на веб-сайті Node.js.
Будьте обережні: багато пакетних менеджерів зараз встановлюють Node.js версії 0.10. У цьому посібнику припускається, що ви використовуєте версію 5.3 або новішу. Ви можете перевірити, яку версію ви використовуєте за допомогою:
node --version
За допомогою команди з node
запускається інтерпретатор JavaScript. За допомогою команди з с npm
до справи береться пакетний менеджер Node.js для встановлення бібліотек, створення нових проектів та запуску скриптів для проекту. На Envato Tuts+ є велика кількість чудових посібників та курсів з Node.js та npm.
Для встановлення бібліотек для сервера вам потрібно виконати наступні команди у програмі Terminal.app або iTerm.app:
npm install express --save npm install handlebars --save npm install moment --save npm install marked --save npm install jade --save npm install morgan --save
Express – фреймворк для розробки веб-застосунків. Він подібний бібліотеці goWeb у Go. Handlebars – шаблонізатор (* ПЗ для комбінування шаблонів з моделлю даних для отримання кінцевих документів) для створення сторінок. Moment – бібліотека для роботи з датами. Marked – відмінний конвертер Markdown до HTML для JavaScript. Jade – рівень абстракції HTML для простішого створення HTML-сторінок. Morgan – бібліотека ПЗ проміжного шару для Express, за допомогою якої генеруються лог-файли (* запис у журналі реєстрації подій, що фіксує дію та подію, що відбулася у комп'ютерній системі) стандарту Apache (* Common Log Format – загальноприйнятий формат лог-файлів).
Альтернативний варіант встановлення необхідних бібліотек – завантажити початкові файли для цього посібника. Після завантаження архіву та його витягнення виконайте наступну команду у головній папці проекту:
npm --install
Завдяки цьому буде встановлено все, що необхідно для створення розглядуваного тут проекту.
nodePress.js
Тепер ми можемо взятися за створення сервера. У кореневій папці проекту створіть файл під назвою nodePress.js, відкрийте його у своєму улюбленому редакторі та починайте додавати туди наступний код: Я буду пояснювати код по мірі його додавання до файлу.
// // Load the libraries used. // var fs = require('fs'); var path = require("path"); var child_process = require('child_process'); var process = require('process'); var express = require('express'); // http://expressjs.com/en/ var morgan = require('morgan'); // https://github.com/expressjs/morgan var Handlebars = require("handlebars"); // http://handlebarsjs.com/ var moment = require("moment"); // http://momentjs.com/ var marked = require('marked'); // https://github.com/chjj/marked var jade = require('jade'); // http://jade-lang.com/
Код починається з підключення всіх потрібних для реалізації сервера бібліотек. Бібліотеки, для яких не вказано коментарі з веб-адресами, постачаються у складі самої платформи Node.js.
// // Setup Global Variables. // var parts = JSON.parse(fs.readFileSync('./server.json', 'utf8')); var styleDir = process.cwd() + '/themes/styling/' + parts['CurrentStyling']; var layoutDir = process.cwd() + '/themes/layouts/' + parts['CurrentLayout']; var siteCSS = null; var siteScripts = null; var mainPage = null;
Потім я задаю значення всіх глобальних змінних та налаштування бібліотеки. Підхід з використанням глобальних змінних не належить до усталеної практики (* загальноприйнята практика, що не є правилом) створення додатків, але працює та прискорює процес розробки.
Змінна parts
– хеш (* колекція пар ключ-значення, причому ключом є рядок), що містить всі частини веб-сторінки. Її контент використовується при створенні кожної сторінки. Контент цієї змінної починається з вмісту файлу server.json, що знаходиться у кореневій папці сервера.
Потім я використовую інформацію з файлу server.json для створення повних шляхів до папок styles
та layouts
, що використовуються для нашого сайту.
Далі у якості значень трьох змінних (siteCSS
, siteScripts
та mainPage
) задається null. У цих глобальних змінних буде міститися контент всіх файлів CSS, JavaScript та головної сторінки. Ці три елементи запитуються найчастіше на всіх серверах. Тому завдяки розміщенню їх у пам'яті ми прискорюємо роботу сайту. Якщо значенням властивості Cache
у файлі server.json є false, то ці елементи зчитуються заново при кожному запиті.
marked.setOptions({ renderer: new marked.Renderer(), gfm: true, tables: true, breaks: false, pedantic: false, sanitize: false, smartLists: true, smartypants: false });
У цьому коді налаштовується бібліотека Marked для отримання HTML з Markdown. Головним чином, тут я активую підтримку таблиць та «розумних» списків (* smartList; список з підтримкою фільтрів).
parts["layout"] = fs.readFileSync(layoutDir + '/template.html', 'utf8'); parts["404"] = fs.readFileSync(styleDir + '/404.html', 'utf8'); parts["footer"] = fs.readFileSync(styleDir + '/footer.html', 'utf8'); parts["header"] = fs.readFileSync(styleDir + '/header.html', 'utf8'); parts["sidebar"] = fs.readFileSync(styleDir + '/sidebar.html', 'utf8'); // // Read in the page parts. // var partFiles = fs.readdirSync(parts['Sitebase'] + "parts/"); partFiles.forEach(function(ele, index, array) { parts[path.basename(ele, path.extname(ele))] = figurePage(parts['Sitebase'] + "parts/" + path.basename(ele, path.extname(ele))); });
Потім до змінної parts
додається контент папок styles
та layouts
. Контент кожного файлу папки parts
, що розташовується у папці site
також завантажується до глобальної змінної parts
. Ім'я файлу без розширення – ім'я, що використовується для збереження контенту файлу. Ці імена замінюються набором інструкцій у макросах (* (від грец. makros - великий, довгий) послідовність команд і/або натискань клавіш, записана макрорегістратором під унікальним ім'ям; блок команд, асоційований із певним унікальним ім'ям) Handlebars.
// // Setup Handlebar's Helpers. // // // HandleBars Helper: save // // Description: This helper expects a // "<name>" "<value>" where the name // is saved with the value for future // expansions. It also returns the // value directly. // Handlebars.registerHelper("save", function(name, text) { // // Local Variables. // var newName = "", newText = ""; // // See if the name and text is in the first argument // with a |. If so, extract them properly. Otherwise, // use the name and text arguments as given. // if(name.indexOf("|") > 0) { var parts = name.split("|"); newName = parts[0]; newText = parts[1]; } else { newName = name; newText = text; } // // Register the new helper. // Handlebars.registerHelper(newName, function() { return newText; }); // // Return the text. // return newText; }); // // HandleBars Helper: date // // Description: This helper returns the date // based on the format given. // Handlebars.registerHelper("date", function(dFormat) { return moment().format(dFormat); }); // // HandleBars Helper: cdate // // Description: This helper returns the date given // in to a format based on the format // given. // Handlebars.registerHelper("cdate", function(cTime, dFormat) { return moment(cTime).format(dFormat); });
У наступній частині коду йде визначення хелперів Handlebars, що будемо використовувати у веб-сервері: save
, date
та cdate
. У хелпері save передбачається створення змінних на сторінці. Ця версія хелпера підтримує версію CMS goPress, в якій у параметрі одночасно присутні ім'я та значення разом, розділені за допомогою “|”. Також можна задати використовувані у хелпері значення за допомогою двох параметрів. Наприклад:
{{save "name|Richard Guay"}} {{save "newName" "Richard Guay"}} Name is: {{name}} newName is: {{newName}}
В обох випадках отримуємо однаковий результат. Мені більше подобається другий підхід, проте у бібліотеці Handlebars, що використовується для Go не передбачається передача більше одного параметра.
За допомогою хелперів date
та cdate
форматується поточне значення дати (date
) або вказане (cdate
) згідно з правилами форматування, що вказані в бібліотеці moment.js. При використанні хелпера cdate
передбачається, що дату, яку потрібно відформатувати, буде передано у якості першого параметра у форматі ISO 8601 (* міжнародний стандарт, що описує обмін інформацією про дати та час, а також деякими іншими даними, що пов'язані з часом; створений в International Organization for Standardization (ISO) та вперше опублікований в 1988).
// // Create and configure the server. // var nodePress = express(); // // Configure middleware. // nodePress.use(morgan('combined'))
Далі в коді створюється зразок Express для налаштування власне сервера. За допомогою функції nodePress.use()
відбувається налаштування ПЗ проміжного шару. ПЗ проміжного шару – будь-який код, що виконується при кожному запиті до серверу. Тут я налаштовую бібліотеку Morgan.js для отримання потрібного формату записів у журналі реєстрації подій.
// // Define the routes. // nodePress.get('/', function(request, response) { setBasicHeader(response); if((parts["Cache"] == true) && (mainPage != null)) { response.send(mainPage); } else { mainPage = page("main"); response.send(mainPage); } }); nodePress.get('/favicon.ico', function(request, response) { var options = { root: parts['Sitebase'] + 'images/', dotfiles: 'deny', headers: { 'x-timestamp': Date.now(), 'x-sent': true } }; response.set("Content-Type", "image/ico"); setBasicHeader(response); response.sendFile('favicon.ico', options, function(err) { if (err) { console.log(err); response.status(err.status).end(); } else { console.log('Favicon was sent:', 'favicon.ico'); } }); }); nodePress.get('/stylesheets.css', function(request, response) { response.set("Content-Type", "text/css"); setBasicHeader(response); response.type("css"); if((parts["Cache"] == true) && (siteCSS != null)) { response.send(siteCSS); } else { siteCSS = fs.readFileSync(parts['Sitebase'] + 'css/final/final.css'); response.send(siteCSS); } }); nodePress.get('/scripts.js', function(request, response) { response.set("Content-Type", "text/javascript"); setBasicHeader(response); if((parts["Cache"] == true) && (siteScripts != null)) { response.send(siteScripts); } else { siteScripts = fs.readFileSync(parts['Sitebase'] + 'js/final/final.js', 'utf8'); response.send(siteScripts); } }); nodePress.get('/images/:image', function(request, response) { var options = { root: parts['Sitebase'] + 'images/', dotfiles: 'deny', headers: { 'x-timestamp': Date.now(), 'x-sent': true } }; response.set("Content-Type", "image/" + path.extname(request.params.image).substr(1)); setBasicHeader(response); response.sendFile(request.params.image, options, function(err) { if (err) { console.log(err); response.status(err.status).end(); } else { console.log('Image was sent:', request.params.image); } }); }); nodePress.get('/posts/blogs/:blog', function(request, response) { setBasicHeader(response); response.send(post("blogs", request.params.blog, "index")); }); nodePress.get('/posts/blogs/:blog/:post', function(request, response) { setBasicHeader(response); response.send(post("blogs", request.params.blog, request.params.post)); }); nodePress.get('/posts/news/:news', function(request, response) { setBasicHeader(response); response.send(post("news", request.params.news, "index")); }); nodePress.get('/posts/news/:news/:post', function(request, response) { setBasicHeader(response); response.send(post("news", request.params.news, request.params.post)); }); nodePress.get('/:page', function(request, response) { setBasicHeader(response); response.send(page(request.params.page)); });
У цьому розділі коду визначаються всі потрібні для реалізації сервера маршрути. У всіх маршрутах виконується функція setBasicHeader()
для встановлення потрібних значень заголовків. У результаті будь-яких запитів на отримання сторінок буде викликано функцію page()
, у той час як в результаті будь-яких запитів на отримання постів буде викликано функцію post()
.
Значенням за налаштуванням для заголовка Content-Type
– HTML. Тому при відправленні CSS, JavaScript та зображень ми спеціально задаємо для Content-Type
відповідне значення.
Також ви можете визначати маршрути за допомогою методів REST (* Representational State Transfer – передача репрезентативного стану; підхід до архітектури мережевих протоколів, які забезпечують доступ до інформаційних ресурсів) put
, delete
та post
. У цьому невеликому сервері використовується тільки метод get
.
// // Start the server. // var addressItems = parts['ServerAddress'].split(':'); var server = nodePress.listen(addressItems[2], function() { var host = server.address().address; var port = server.address().port; console.log('nodePress is listening at http://%s:%s', host, port); });
Нам залишилось тепер, перед визначенням різних використовуваних у коді функцій, тільки запустити сервер. У файлі server.json міститься DNS-ім'я (у нас localhost
) та порт для сервера. Після парсингу (* розбиття рядка тексту на складові частини) у функції listen()
використовується номер порту для запуску сервера. Після відкриття порту сервера завдяки скрипту до консолі виводиться адреса та порт сервера.
// // Function: setBasicHeader // // Description: This function will set the basic header information // needed. // // Inputs: // response The response object // function setBasicHeader(response) { response.append("Cache-Control", "max-age=2592000, cache"); response.append("Server", "nodePress - a CMS written in node from Custom Computer Tools: http://customct.com."); }
Перша функція, яку будемо визначати, – setBasicHeader()
. За допомогою цієї функції встановлюється значення спеціального заголовку відповіді для повідомлення браузерові про потребу гешування сторінки на один місяць. Також завдяки ній браузеру повідомляється, що сервер – це сервер nodePress. Якщо би ви хотіли додати ще якісь стандартні заголовки, то додали би їх за допомогою функції response.append()
.
// // Function: page // // Description: This function processes a page request // // Inputs: // page The requested page // function page(page) { // // Process the given page using the standard layout. // return (processPage(parts["layout"], parts['Sitebase'] + "pages/" + page)); }
У функції page()
шаблон розмітки сторінки та адреса сторінки на сервері передаються до функції processPage()
.
// // Function: post // // Description: This function processes a post request // // Inputs: // type The type of post. // cat The category of the post. // post The requested post // function post(type, cat, post) { // // Process the post given the type and the post name. // return (processPage(parts["layout"], parts['Sitebase'] + "posts/" + type + "/" + cat + "/" + post)); }
Функція post()
подібна функції page()
, за винятком того, що при створенні поста використовується більше інформації. У створюваних у цій серії посібників серверах пост містить тип
, категорію та власне пост
. Типом поста є або blogs
, або news
. Категорія – flatcms
. Оскільки ними є імена директорій, то ви можете давати їх будь-які імена. Просто підберіть ім'я таким чином, щоб воно співпадало з тим, що використовується у вашій файловій системі.
// // Function: processPage // // Description: This function processes a page for the CMS. // // Inputs: // layout The layout to use for the page. // page Path to the page to render. // function processPage(layout, page) { // // Get the pages contents and add to the layout. // var context = {}; context = MergeRecursive(context, parts); context['content'] = figurePage(page); context['PageName'] = path.basename(page, path.extname(page)); // // Load page data. // if(fileExists(page + ".json")) { // // Load the page's data file and add it to the data structure. // context = MergeRecursive(context, JSON.parse(fs.readFileSync(page + '.json', 'utf8'))); } // // Process Handlebars codes. // var template = Handlebars.compile(layout); var html = template(context); // // Process all shortcodes. // html = processShortCodes(html); // // Run through Handlebars again. // template = Handlebars.compile(html); html = template(context); // // Return results. // return (html); }
Функція processPage()
приймає макет та шлях до потрібного для відображення контенту сторінки. Код функції починається зі створення локальної копії глобальної змінної parts
та додавання ключа, використовуваного для розміщення “контенту”, значенням якого є результат виконання функції figurePage()
. Далі у якості значення PageName
задається ім'я сторінки.
Далі відбувається компіляція контенту сторінки у шаблон з розміткою за допомогою Handlebars. Потім завдяки функції processShortCodes()
весь скорочений код на сторінці буде перетворено у повний. Після цього шаблонізатор Handlebars проганяє код через себе ще раз. І потім браузер отримує результат.
// // Function: processShortCodes // // Description: This function takes a string and // processes all of the shortcodes in // the string. // // Inputs: // content String to process // function processShortCodes(content) { // // Create the results variable. // var results = ""; // // Find the first match. // var scregFind = /\-\[([^\]]*)\]\-/i; var match = scregFind.exec(content); if (match != null) { results += content.substr(0,match.index); var scregNameArg = /(\w+)(.*)*/i; var parts = scregNameArg.exec(match[1]); if (parts != null) { // // Find the closing tag. // var scregClose = new RegExp("\\-\\[\\/" + parts[1] + "\\]\\-"); var left = content.substr(match.index + 4 + parts[1].length); var match2 = scregClose.exec(left); if (match2 != null) { // // Process the enclosed shortcode text. // var enclosed = processShortCodes(content.substr(match.index + 4 + parts[1].length, match2.index)); // // Figure out if there were any arguments. // var args = ""; if (parts.length == 2) { args = parts[2]; } // // Execute the shortcode. // results += shortcodes[parts[1]](args, enclosed); // // Process the rest of the code for shortcodes. // results += processShortCodes(left.substr(match2.index + 5 + parts[1].length)); } else { // // Invalid shortcode. Return full string. // results = content; } } else { // // Invalid shortcode. Return full string. // results = content; } } else { // // No shortcodes found. Return the string. // results = content; } return (results); }
Функція processShortCodes()
приймає контент веб-сторінки у якості рядка та здійснює пошук усіх частин коду у скороченому вигляді. Скорочений код – це блок коду, подібний тегам HTML. Прикладом міг би стати:
-[box]- <p>This is inside a box</p> -[/box]-
У цьому коді є скорочений код для box
навколо параграфа HTML. Там, де в HTML використовується <
и >
, у скороченому коді використовується -[
и ]-
. Після імені може при потребі додаватися рядок з аргументами для скороченого коду.
У функції processShortCodes()
відбувається пошук скороченого коду, отримання його імені та аргументів, знаходження його кінці для отримання контенту, оброблення цього контенту, виконання скороченого коду з урахуванням аргументів та контенту, додавання результат до фінальної сторінки та пошук решти частин скороченого коду на сторінці. Уведення циклу здійснюється завдяки рекурсивному виклику функції.
// // Define the shortcodes function array. // var shortcodes = { 'box': function(args, inside) { return ("<div class='box'>" + inside + "</div>"); }, 'Column1': function(args, inside) { return ("<div class='col1'>" + inside + "</div>"); }, 'Column2': function(args, inside) { return ("<div class='col2'>" + inside + "</div>"); }, 'Column1of3': function(args, inside) { return ("<div class='col1of3'>" + inside + "</div>"); }, 'Column2of3': function(args, inside) { return ("<div class='col2of3'>" + inside + "</div>"); }, 'Column3of3': function(args, inside) { return ("<div class='col3of3'>" + inside + "</div>"); }, 'php': function(args, inside) { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: php'>" + inside + "</pre></div>"); }, 'js': function(args, inside) { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: javascript'>" + inside + "</pre></div>"); }, 'html': function(args, inside) { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: html'>" + inside + "</pre></div>"); }, 'css': function(args, inside) { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: css'>" + inside + "</pre></div>"); } };
У цій частині йде json-структура скороченого коду
, у якій визначаються імена скороченого коду зі зв'язаними з ними функціями. Всі функції для скороченого коду приймають два параметри: args
та inside
. аrgs
– все, що йде після імені та пробілу до кінця тегу. inside
– все, що розташовується між відкриваючим та закриваючим тегами скороченого коду. Це базові функції, проте ви можете створити скорочений код для виконання будь-чого на JavaScript, що спадає вам на думку.
// // Function: figurePage // // Description: This function figures the page type // and loads the contents appropriately // returning the HTML contents for the page. // // Inputs: // page The page to load contents. // function figurePage(page) { var result = ""; if (fileExists(page + ".html")) { // // It's an HTML file. Read it in and send it on. // result = fs.readFileSync(page + ".html"); } else if (fileExists(page + ".amber")) { // // It's a jade file. Convert to HTML and send it on. I // am still using the amber extension for compatibility // to goPress. // var jadeFun = jade.compileFile(page + ".amber", {}); // Render the function var result = jadeFun({}); } else if (fileExists(page + ".md")) { // // It's a markdown file. Convert to HTML and send // it on. // result = marked(fs.readFileSync(page + ".md").toString()); // // This undo marked's URI encoding of quote marks. // result = result.replace(/\"\;/g,"\""); } return (result); }
Функція figurePage()
отримує повний шлях на сторінку на сервері. Потім у цій функції перевіряється формат сторінки (HTML, Markdown або Jade) на основі розширення. Я як і раніше використовую .amber для коду на Jade, оскільки я використовував бібліотеку Аmber при створенні сервера goPress. Весь контент на Markdown та Jade транлюється в HTML перед його передачею рутинній функції (* return()). Оскільки процесор Markdown перетворює всі лапки у "
, то я перетворю їх назад перед поверненням результату.
// // Function: fileExists // // Description: This function returns a boolean true if // the file exists. Otherwise, false. // // Inputs: // filePath Path to a file in a string. // function fileExists(filePath) { try { return fs.statSync(filePath).isFile(); } catch (err) { return false; } }
Функція fileExists()
– заміна для функції fs.exists()
, що буда частиною бібліотеки fs
Node.js (* до версії 1.0.0). У ній використовується функція fs.statSync()
, щоб спробувати отримати статус файлу. У разі помилки функція повертає false
. Інакше – true
.
// // Function: MergeRecursive // // Description: Recursively merge properties of two objects // // Inputs: // obj1 The first object to merge // obj2 The second object to merge // function MergeRecursive(obj1, obj2) { for (var p in obj2) { try { // Property in destination object set; update its value. if (obj2[p].constructor == Object) { obj1[p] = MergeRecursive(obj1[p], obj2[p]); } else { obj1[p] = obj2[p]; } } catch (e) { // Property in destination object not set; create it and set its value. obj1[p] = obj2[p]; } } return obj1; }
Переходимо до останньої функції – MergeRecursive()
. У ній відбувається копіювання другого переданого об'єкта до першого. Я її використовую для копіювання вмісту головної змінної parts
до локальної копії перед додаванням частин, властивим різним сторінкам.
Запускаємо сервер локально:
Після зберігання файлу ви можете запустити сервер за допомогою:
node nodePress.js
У якості альтернативного варіанта ви можете використати вказаний у файлі package.json скрипт, що запускається за допомогою команди npm
. Запуск скриптів за допомогою npm виконується наступним чином:
npm start
За допомогою цієї команди буде виконано скрипт start
, який вказано у файлі package.json.



Перейдіть у вашому браузері за http://localhost:8080
і ви побачите сторінку, показану вище. Ви, напевно, помітили, що я додав додатковий код для тестування сайту на головну сторінку. Зі внесеними на сторінки змінами ви можете ознайомитися у приєднаних файлах. Це, головним чином, просто деякі невеликі поправки, внесені для повнішого тестування функціональних можливостей та підгонки коду під вимоги різних бібліотек. Найзначніша різниця полягає в тому, що в бібліотеці Jade на використовується $
при найменування змінних, а в Amber використовується.
Завершення
Тепер ми маємо ідентичні CMS, що працюють на основі плоскої файлової системи, реалізовані за допомогою Gо та Node.js. Ми скористалися тільки базовими можливостями цієї платформи. Поекспериментуйте та спробуйте щось нове. Це саме цікаве при створенні власного сервера.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weekly