Статьи

Как собрать и структурировать приложение Node.js MVC

В нетривиальном приложении архитектура так же важна, как и качество самого кода. У нас могут быть хорошо написанные фрагменты кода, но если у нас нет хорошей организации, нам будет трудно из-за сложности. Нет необходимости ждать, пока проект будет на полпути, чтобы начать думать об архитектуре. Лучшее время перед началом, используя наши цели в качестве маяков для нашего выбора.

Node.js не имеет де-факто фреймворка с сильными взглядами на архитектуру и организацию кода так же, как в Ruby, например, фреймворк Rails. Таким образом, может быть сложно начать создавать полноценные веб-приложения с Node.

В этой статье мы собираемся создать базовую функциональность приложения для создания заметок с использованием архитектуры MVC. Для этого мы будем использовать среду Hapi.js для Node.js и SQLite в качестве базы данных, используя Sequelize.js , а также другие небольшие утилиты для ускорения нашей разработки. Мы собираемся создавать представления, используя Pug , язык шаблонов.

Смотреть сборки плагинов с Hapi.js
Фреймворк веб-приложений с Node — нужно ли говорить больше

Внутри монитора куклы управляют приложением Node.js MVC.

Что такое MVC?

Model-View-Controller (или MVC), вероятно, является одной из самых популярных архитектур для приложений. Как и во многих других интересных вещах в компьютерной истории, модель MVC была разработана в PARC для языка Smalltalk как решение проблемы организации приложений с графическим пользовательским интерфейсом. Он был создан для настольных приложений, но с тех пор идея была адаптирована к другим средам, включая Интернет.

Мы можем описать архитектуру MVC простыми словами:

  • Модель : часть нашего приложения, которая будет работать с базой данных или любыми функциями, связанными с данными.
  • Вид : все, что увидит пользователь. В основном страницы, которые мы собираемся отправить клиенту.
  • Контроллер : логика нашего сайта и связь между моделями и представлениями. Здесь мы вызываем наши модели для получения данных, а затем помещаем эти данные в наши представления для отправки пользователям.

Наше приложение позволит нам публиковать, просматривать, редактировать и удалять текстовые заметки. У него не будет другой функциональности, но, поскольку у нас уже есть солидная архитектура, у нас не будет больших проблем с добавлением вещей позже.

Вы можете проверить окончательное приложение в прилагаемом репозитории GitHub , чтобы получить общий обзор структуры приложения.

Укладка фундамента

Первым шагом при создании любого приложения Node.js является создание файла package.json , который будет содержать все наши зависимости и сценарии. Вместо того, чтобы создавать этот файл вручную, npm может выполнить эту работу за нас с помощью команды init :

 npm init -y 

После завершения процесса вы получите файл package.json готовый к использованию.

Примечание. Если вы не знакомы с этими командами, ознакомьтесь с Руководством для начинающих по npm .

Мы собираемся приступить к установке Hapi.js — фреймворка для этого урока. Он обеспечивает хороший баланс между простотой, стабильностью и доступностью функций, которые будут хорошо работать для нашего варианта использования (хотя есть и другие варианты, которые также будут работать просто отлично).

 npm install --save hapi hoek 

Эта команда загрузит последнюю версию Hapi.js и добавит ее в наш файл package.json в качестве зависимости. Он также скачает служебную библиотеку Hoek , которая, среди прочего, поможет нам написать более короткие обработчики ошибок.

Теперь мы можем создать наш входной файл — веб-сервер, который будет запускать все. server.js файл server.js в каталоге вашего приложения и весь следующий код:

 'use strict'; const Hapi = require('hapi'); const Hoek = require('hoek'); const Settings = require('./settings'); const server = new Hapi.Server(); server.connection({ port: Settings.port }); server.route({ method: 'GET', path: '/', handler: (request, reply) => { reply('Hello, world!'); } }); server.start((err) => { Hoek.assert(!err, err); console.log(`Server running at: ${server.info.uri}`); }); 

Это будет основой нашего приложения.

Во-первых, мы указываем, что мы будем использовать строгий режим , который является обычной практикой при использовании фреймворка Hapi.js.

Затем мы включаем наши зависимости и создаем новый объект сервера, для которого мы устанавливаем порт подключения равным 3000 (порт может быть любым числом от 1023 до 65535 ).

Наш первый маршрут для нашего сервера будет работать в качестве теста, чтобы увидеть, все ли работает, поэтому нам достаточно сообщения «Привет, мир!». В каждом маршруте мы должны определить HTTP-метод и путь (URL), на которые он будет отвечать, и обработчик, который является функцией, которая будет обрабатывать HTTP-запрос. Функция-обработчик может принимать два аргумента: request и reply . Первый содержит информацию о HTTP-вызове, а второй предоставит нам методы для обработки нашего ответа на этот вызов.

Наконец, мы запускаем наш сервер с server.start метода server.start . Как видите, мы можем использовать Hoek, чтобы улучшить нашу обработку ошибок, сделав ее короче. Это совершенно необязательно, поэтому не стесняйтесь опускать его в своем коде, просто обязательно обрабатывайте любые ошибки.

Хранение наших настроек

Рекомендуется хранить переменные конфигурации в отдельном файле. Этот файл экспортирует объект JSON, содержащий наши данные, где каждый ключ назначается из переменной среды, но не забывая запасное значение.

В этом файле мы также можем иметь различные настройки в зависимости от нашей среды (например, разработка или производство). Например, у нас может быть экземпляр SQLite в памяти для целей разработки, но настоящий файл базы данных SQLite на производстве.

Выбор настроек в зависимости от текущей среды довольно прост. Поскольку в нашем файле также есть переменная env которая будет содержать либо development либо production , мы можем сделать что-то вроде следующего, чтобы получить настройки базы данных (например):

 const dbSettings = Settings[Settings.env].db; 

Таким образом, dbSettings будет содержать настройку базы данных в памяти, когда переменная env находится в development , или будет содержать путь к файлу базы данных, когда переменная env является production .

Кроме того, мы можем добавить поддержку файла .env , где мы можем хранить переменные нашей среды локально для целей разработки; это достигается с помощью пакета, такого как dotenv для Node.js, который будет считывать файл .env из корня нашего проекта и автоматически добавлять найденные значения в среду. Вы можете найти пример в хранилище dotenv .

Примечание. Если вы также решили использовать файл .env , убедитесь, что вы установили пакет с помощью npm install -s dotenv и добавили его в .gitignore чтобы не публиковать конфиденциальную информацию.

Наш файл settings.js будет выглядеть так:

 // This will load our .env file and add the values to process.env, // IMPORTANT: Omit this line if you don't want to use this functionality require('dotenv').config({silent: true}); module.exports = { port: process.env.PORT || 3000, env: process.env.ENV || 'development', // Environment-dependent settings development: { db: { dialect: 'sqlite', storage: ':memory:' } }, production: { db: { dialect: 'sqlite', storage: 'db/database.sqlite' } } }; 

Теперь мы можем запустить наше приложение, выполнив следующую команду и перейдя к localhost:3000 в нашем веб-браузере.

 node server.js 

Примечание: этот проект был протестирован на Node v6. Если вы получили какие-либо ошибки, убедитесь, что у вас установлена ​​обновленная версия.

Определение маршрутов

Определение маршрутов дает нам обзор функций, поддерживаемых нашим приложением. Чтобы создать наши дополнительные маршруты, нам просто нужно скопировать структуру маршрута, которую мы уже имеем в нашем файле server.js , изменив содержимое каждого из них.

Давайте начнем с создания нового каталога с именем lib в нашем проекте. Здесь мы собираемся включить все компоненты JS. Внутри lib давайте создадим файл routes.js и добавим следующий контент:

 'use strict'; module.exports = [ // We're going to define our routes here ]; 

В этом файле мы экспортируем массив объектов, которые содержат каждый маршрут нашего приложения. Чтобы определить первый маршрут, добавьте следующий объект в массив:

 { method: 'GET', path: '/', handler: (request, reply) => { reply('All the notes will appear here'); }, config: { description: 'Gets all the notes available' } }, 

Наш первый маршрут предназначен для домашней страницы ( / ), и поскольку он будет возвращать только информацию, мы назначаем ему метод GET . Пока это только даст нам сообщение. All the notes will appear here , которые мы собираемся изменить позже для функции контроллера. Поле description в разделе config предназначено только для документации.

Затем мы создаем четыре маршрута для наших заметок в /note/ path. Поскольку мы создаем приложение CRUD , нам потребуется один маршрут для каждого действия с соответствующим методом HTTP .

Добавьте следующие определения рядом с предыдущим маршрутом:

 { method: 'POST', path: '/note', handler: (request, reply) => { reply('New note'); }, config: { description: 'Adds a new note' } }, { method: 'GET', path: '/note/{slug}', handler: (request, reply) => { reply('This is a note'); }, config: { description: 'Gets the content of a note' } }, { method: 'PUT', path: '/note/{slug}', handler: (request, reply) => { reply('Edit a note'); }, config: { description: 'Updates the selected note' } }, { method: 'GET', path: '/note/{slug}/delete', handler: (request, reply) => { reply('This note no longer exists'); }, config: { description: 'Deletes the selected note' } }, 

Мы сделали то же самое, что и в предыдущем определении маршрута, но на этот раз мы изменили метод, чтобы соответствовать действию, которое мы хотим выполнить.

Единственным исключением является удаление маршрута. В этом случае мы определим его с помощью метода GET а не DELETE и добавим дополнительный /delete в путь. Таким образом, мы можем вызвать действие удаления, просто посетив соответствующий URL.

Примечание. Если вы планируете реализовать строгий интерфейс REST, вам придется использовать метод DELETE и /delete часть пути /delete .

Мы можем назвать параметры в пути, заключив слово в скобки ( {} ). Поскольку мы собираемся идентифицировать заметки по слагу, мы добавляем {slug} к каждому пути, за исключением маршрута PUT. Нам это не нужно, потому что мы не собираемся взаимодействовать с конкретной заметкой, а создадим ее.

Вы можете прочитать больше о маршрутах Hapi.js в официальной документации .

Теперь мы должны добавить наши новые маршруты в файл server.js . Давайте импортируем файл маршрутов вверху файла:

 const Routes = require('./lib/routes'); 

и замените наш текущий тестовый маршрут следующим:

 server.route(Routes); 

Построение моделей

Модели позволяют нам определять структуру данных и все функции для работы с ними.

В этом примере мы собираемся использовать базу данных SQLite с Sequelize.js, которая предоставит нам лучший интерфейс с использованием техники ORM ( Object-Relational Mapping ). Это также предоставит нам независимый от базы данных интерфейс.

Настройка базы данных

Для этого раздела мы будем использовать Sequelize.js и SQlite . Вы можете установить и включить их как зависимости, выполнив следующую команду:

 npm install -s sequelize sqlite3 

Теперь создайте каталог models внутри lib/ с файлом index.js который будет содержать базу данных и настройку Sequelize.js, а также следующий контент:

 'use strict'; const Fs = require('fs'); const Path = require('path'); const Sequelize = require('sequelize'); const Settings = require('../../settings'); // Database settings for the current environment const dbSettings = Settings[Settings.env].db; const sequelize = new Sequelize(dbSettings.database, dbSettings.user, dbSettings.password, dbSettings); const db = {}; // Read all the files in this directory and import them as models Fs.readdirSync(__dirname) .filter((file) => (file.indexOf('.') !== 0) && (file !== 'index.js')) .forEach((file) => { const model = sequelize.import(Path.join(__dirname, file)); db[model.name] = model; }); db.sequelize = sequelize; db.Sequelize = Sequelize; module.exports = db; 

Сначала мы включаем модули, которые будем использовать:

  • Fs , чтобы прочитать файлы в папке моделей , которая будет содержать все модели.
  • Path , чтобы присоединить путь к каждому файлу в текущем каталоге.
  • Sequelize , это позволит нам создать новый экземпляр Sequelize.
  • Settings , которые содержат данные нашего файла settings.js из корня нашего проекта.

Затем мы создаем новую переменную sequelize которая будет содержать экземпляр Sequelize с настройками нашей базы данных для текущей среды. Мы собираемся использовать sequelize чтобы импортировать все модели и сделать их доступными в нашем объекте db .

Объект db будет экспортирован и будет содержать методы нашей базы данных для каждой модели; он будет доступен в нашем приложении, когда нам нужно что-то сделать с нашими данными.

Чтобы загрузить все модели, вместо того, чтобы определять их вручную, мы ищем все файлы в каталоге models (за исключением файла index.js ) и загружаем их, используя функцию import . Возвращенный объект предоставит нам методы CRUD, которые мы затем добавим в объект db .

В конце мы добавляем sequelize и Sequelize как часть нашего объекта db , первый будет использоваться в нашем файле server.js для подключения к базе данных перед запуском сервера, а второй включен для удобства, если вы нужно это и в других файлах.

Создание нашей модели Note

В этом разделе мы собираемся использовать пакет Moment.js , чтобы помочь с форматированием даты. Вы можете установить его и включить в качестве зависимости с помощью следующей команды:

 npm install -s moment 

Теперь мы собираемся создать файл note.js внутри каталога models , который будет единственной моделью в нашем приложении; это предоставит нам всю необходимую нам функциональность.

Добавьте следующее содержимое в этот файл:

 'use strict'; const Moment = require('moment'); module.exports = (sequelize, DataTypes) => { let Note = sequelize.define('Note', { date: { type: DataTypes.DATE, get: function () { return Moment(this.getDataValue('date')).format('MMMM Do, YYYY'); } }, title: DataTypes.STRING, slug: DataTypes.STRING, description: DataTypes.STRING, content: DataTypes.STRING }); return Note; }; 

Мы экспортируем функцию, которая принимает экземпляр sequelize для определения модели, и объект DataTypes со всеми типами, доступными в нашей базе данных.

Далее мы определяем структуру наших данных, используя объект, где каждый ключ соответствует столбцу базы данных, а значение ключа определяет тип данных, которые мы собираемся хранить. Вы можете увидеть список типов данных в документации Sequelize.js . Таблицы в базе данных будут создаваться автоматически на основе этой информации.

В случае столбца даты мы также определяем, как Sequelize должен возвращать значение, используя функцию get (ключ get ). Мы указываем, что перед возвратом информации ее необходимо сначала пропустить через утилиту Moment, чтобы она была отформатирована в более удобном для чтения виде ( MMMM Do, YYYY ).

Примечание. Несмотря на то, что мы получаем простую и удобную для чтения строку даты, она сохраняется как точный продукт строки даты объекта Date в JavaScript. Так что это не разрушительная операция.

Наконец, мы возвращаем нашу модель.

Синхронизация базы данных

Теперь нам нужно синхронизировать нашу базу данных, прежде чем мы сможем использовать ее в нашем приложении. В server.js импортируйте модели вверху файла:

 // Import the index.js file inside the models directory const Models = require('./lib/models/'); 

Далее замените следующий блок кода:

 server.start((err) => { Hoek.assert(!err, err); console.log(`Server running at: ${server.info.uri}`); }); 

с этим:

 Models.sequelize.sync().then(() => { server.start((err) => { Hoek.assert(!err, err); console.log(`Server running at: ${server.info.uri}`); }); }); 

Этот код будет синхронизировать модели с нашей базой данных, и, как только это будет сделано, сервер будет запущен.

Сборка контроллеров

Контроллеры — это функции, которые принимают объекты запроса и ответа от Hapi.js. Объект request содержит информацию о запрашиваемом ресурсе, и мы используем reply для возврата информации клиенту.

В нашем приложении мы собираемся сейчас возвращать только объект JSON, но мы добавим представления, как только мы их построим.

Мы можем думать о контроллерах как о функциях, которые объединят наши модели с нашими представлениями; они будут связываться с нашими моделями для получения данных, а затем возвращать эти данные в представление.

Домашний контроллер

Первый контроллер, который мы собираемся построить, будет обрабатывать домашнюю страницу нашего сайта. Создайте файл home.js каталоге lib/controllers со следующим содержимым:

 'use strict'; const Models = require('../models/'); module.exports = (request, reply) => { Models.Note .findAll({ order: [['date', 'DESC']] }) .then((result) => { reply({ data: { notes: result }, page: 'Home—Notes Board', description: 'Welcome to my Notes Board' }); }); }; 

Сначала мы получаем все заметки в нашей базе данных, используя метод findAll нашей модели. Эта функция вернет обещание, и, если оно разрешится, мы получим массив, содержащий все заметки в нашей базе данных.

Мы можем расположить результаты в порядке убывания, используя параметр order в объекте параметров, передаваемый методу findAll , поэтому последний элемент появится первым. Вы можете проверить все доступные опции в документации Sequelize.js .

Как только у нас будет домашний контроллер, мы можем отредактировать наш файл routes.js Сначала мы импортируем модуль вверху файла, рядом с импортом модуля Path :

 const Home = require('./controllers/home'); 

Затем мы добавляем контроллер, который мы только что сделали, в массив:

 { method: 'GET', path: '/', handler: Home, config: { description: 'Gets all the notes available' } }, 

Бойлер контроллера ноты

Поскольку мы собираемся идентифицировать наши заметки с помощью slug, мы можем сгенерировать их, используя заголовок примечания и библиотеку slug , поэтому давайте установим его и включим в качестве зависимости с помощью следующей команды:

 npm install -s slug 

Последний контроллер, который мы должны определить в нашем приложении, позволит нам создавать, читать, обновлять и удалять заметки.

Мы можем приступить к созданию файла note.js каталоге lib/controllers и добавить следующее содержимое:

 'use strict'; const Models = require('../models/'); const Slugify = require('slug'); const Path = require('path'); module.exports = { // Here we're going to include our functions that will handle each request in the routes.js file. }; 

Функция «создать»

Чтобы добавить примечание в нашу базу данных, мы напишем функцию create которая обернет метод create в нашей модели, используя данные, содержащиеся в объекте полезной нагрузки.

Добавьте следующее в объект, который мы экспортируем:

 create: (request, reply) => { Models.Note .create({ date: new Date(), title: request.payload.noteTitle, slug: Slugify(request.payload.noteTitle, {lower: true}), description: request.payload.noteDescription, content: request.payload.noteContent }) .then((result) => { // We're going to generate a view later, but for now lets just return the result. reply(result); }); }, 

Как только заметка будет создана, мы вернем данные заметки и отправим их клиенту в формате JSON, используя функцию reply .

Сейчас мы просто возвращаем результат, но как только мы создадим представления в следующем разделе, мы сможем сгенерировать HTML с новой заметкой и динамически добавить ее на клиенте. Хотя это не является абсолютно необходимым и будет зависеть от того, как вы собираетесь обрабатывать свою внешнюю логику, мы собираемся вернуть блок HTML, чтобы упростить логику на клиенте.

Также обратите внимание, что дата генерируется на лету, когда мы выполняем функцию, используя new Date() .

Функция «чтения»

Для поиска только одного элемента мы используем метод findOne в нашей модели. Поскольку мы идентифицируем заметки по их слагу, фильтр where должен содержать слаг, предоставленный клиентом в URL ( http://localhost:3000/note/:slug: .

 read: (request, reply) => { Models.Note .findOne({ where: { slug: request.params.slug } }) .then((result) => { reply(result); }); }, 

Как и в предыдущей функции, мы просто вернем результат, который будет объектом, содержащим информацию заметки. Представления будут использоваться после того, как мы создадим их в разделе Построение представлений .

Функция «обновление»

Чтобы обновить заметку, мы используем метод update нашей модели. Он принимает два объекта: новые значения, которые мы собираемся заменить, и параметры, содержащие фильтр where с слагом примечаний, который является примечанием, которое мы собираемся обновить.

 update: (request, reply) => { const values = { title: request.payload.noteTitle, description: request.payload.noteDescription, content: request.payload.noteContent }; const options = { where: { slug: request.params.slug } }; Models.Note .update(values, options) .then(() => { Models.Note .findOne(options) .then((result) => { reply(result); }); }); }, 

После обновления наших данных, поскольку наша база данных не возвращает обновленную заметку, мы можем снова найти измененную заметку, чтобы вернуть ее клиенту, поэтому мы можем показать обновленную версию, как только изменения будут внесены.

Функция «удалить»

Контроллер удаления удалит примечание, предоставив параметр для функции destroy нашей модели. Затем, после удаления заметки, мы перенаправляем на домашнюю страницу. Для этого мы используем функцию перенаправления объекта ответа Hapi.js.

 delete: (request, reply) => { Models.Note .destroy({ where: { slug: request.params.slug } }) .then(() => reply.redirect('/')); } 

Использование контроллера Note в наших маршрутах

На этом этапе у нас должен быть готов файл контроллера заметок со всеми действиями CRUD. Но чтобы использовать их, мы должны включить его в наш файл маршрутов.

Во-первых, давайте импортируем наш контроллер в верхней части файла routes.js :

 const Note = require('./controllers/note'); 

Мы должны заменить каждый обработчик нашими новыми функциями, поэтому у нас должен быть наш файл маршрутов следующим образом:

 { method: 'POST', path: '/note', handler: Note.create, config: { description: 'Adds a new note' } }, { method: 'GET', path: '/note/{slug}', handler: Note.read, config: { description: 'Gets the content of a note' } }, { method: 'PUT', path: '/note/{slug}', handler: Note.update, config: { description: 'Updates the selected note' } }, { method: 'GET', path: '/note/{slug}/delete', handler: Note.delete, config: { description: 'Deletes the selected note' } }, 

Примечание: мы включаем наши функции без () в конце, потому что мы ссылаемся на наши функции, не вызывая их.

Создание представлений

На этом этапе наш сайт получает HTTP-вызовы и отвечает объектами JSON. Чтобы сделать его полезным для всех, мы должны создать страницы, которые хорошо отображают нашу информацию.

В этом примере мы собираемся использовать язык шаблонов Pug, хотя это не является обязательным, и мы можем использовать другие языки с Hapi.js. Кроме того, мы собираемся использовать плагин Vision, чтобы включить функциональность просмотра на нашем сервере.

Примечание. Если вы не знакомы с Pug (ранее Jade), ознакомьтесь с нашим руководством по Jade для начинающих .

Вы можете установить пакеты с помощью следующей команды:

 npm install -s vision pug 

Компонент примечания

Во-первых, мы собираемся создать наш компонент заметок, который будет повторно использоваться в наших представлениях. Кроме того, мы собираемся использовать этот компонент в некоторых наших функциях контроллера для создания заметки на лету в серверной части, чтобы упростить логику на клиенте.

Создайте файл в lib/views/components именем note.pug со следующим содержимым:

 article h2: a(href=`/note/${note.slug}`)= note.title small Published on #{note.date} p= note.content 

Он состоит из названия заметки, даты публикации и содержания заметки.

Базовая планировка

Базовый макет содержит общие элементы наших страниц; или, другими словами, для нашего примера, все, что не является содержанием. Создайте файл в lib/views/ именем layout.pug со следующим содержимым:

 doctype html html(lang='en') head meta(charset='utf-8') meta(http-equiv='x-ua-compatible' content='ie=edge') title= page meta(name='description' content=description) meta(name='viewport' content='width=device-width, initial-scale=1') link(href='https://fonts.googleapis.com/css?family=Gentium+Book+Basic:400,400i,700,700i|Ubuntu:500' rel='stylesheet') link(rel='stylesheet' href='/styles/main.css') body block content script(src='https://code.jquery.com/jquery-3.1.1.min.js' integrity='sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=' crossorigin='anonymous') script(src='/scripts/jquery.modal.js') script(src='/scripts/main.js') 

Содержимое других страниц будет загружено вместо block content . Также обратите внимание, что мы отобразим переменную страницы в элементе title и переменную description в элементе meta(name='description') . Мы создадим эти переменные в наших маршрутах позже.

Внизу страницы мы также включаем три файла JS, jQuery , jQuery Modal и файл main.js, который будет содержать весь наш пользовательский код JS для внешнего интерфейса. Обязательно загрузите эти пакеты и поместите их в каталог static/public/scripts/ . Мы собираемся сделать их общедоступными в разделе « Обслуживание статических файлов ».

Домашний вид

На нашей домашней странице мы покажем список, содержащий все заметки в нашей базе данных, и кнопку, которая покажет модальное окно с формой, которая позволяет нам создавать новую заметку через Ajax.

Создайте файл в lib/views именем home.pug со следующим содержимым:

 extends layout block content header(container) h1 Notes Board nav ul // This will show a modal window with a form to send new notes li: a(href='#note-form' rel='modal:open') Publish main(container).notes-list // We loop over all the notes received from our controller rendering our note component with each entry each note in data.notes include components/note // Form to add a new note, this is used by our controller `create` function. form(action='/note' method='POST').note-form#note-form p: input(name='noteTitle' type='text' placeholder='Title…') p: input(name='noteDescription' type='text' placeholder='Short description…') p: textarea(name='noteContent') Write here the content of the new note… p._text-right: input(type='submit' value='Submit') 

Вид заметки

Страница заметки очень похожа на домашнюю страницу, но в этом случае мы показываем меню с параметрами, относящимися к текущей заметке, содержанием заметки и той же формой, что и на домашней странице, но с уже заполненной информацией текущей заметки. так что это там, когда мы обновляем его.

Создайте файл в lib/views именем note.pug со следующим содержимым:

 extends layout block content header(container) h1 Notes Board nav ul li: a(href='/') Home li: a(href='#note-form' rel='modal:open') Update li: a(href=`/note/${note.slug}/delete`) Delete main(container).note-content include components/note form(action=`/note/${note.slug}` method='PUT').note-form#note-form p: input(name='noteTitle' type='text' value=note.title) p: input(name='noteDescription' type='text' value=note.description) p: textarea(name='noteContent')= note.content p._text-right: input(type='submit' value='Update') 

JavaScript на клиенте

Для создания и обновления заметок мы используем функциональность Ajax jQuery. Хотя это не является строго необходимым, я чувствую, что это обеспечивает лучший опыт для пользователя.

Это содержимое нашего файла main.js каталоге static/public/scripts/ :

 $('#note-form').submit(function (e) { e.preventDefault(); var form = { url: $(this).attr('action'), type: $(this).attr('method') }; $.ajax({ url: form.url, type: form.type, data: $(this).serialize(), success: function (result) { $.modal.close(); if (form.type === 'POST') { $('.notes-list').prepend(result); } else if (form.type === 'PUT') { $('.note-content').html(result); } } }); }); 

Каждый раз, когда пользователь отправляет форму в модальном окне, мы получаем информацию из элементов формы и отправляем ее нашему фону в зависимости от URL-адреса действия и метода ( POST или PUT ). Затем мы получим результат в виде блока HTML, содержащего наши новые данные заметок. Когда мы добавляем заметку, мы просто добавляем ее в начало списка на главной странице, а при обновлении заметки мы заменяем ее содержимое на новое в представлении заметки.

Добавление поддержки представлений на сервере

Чтобы использовать наши представления, мы должны включить их в наши контроллеры и добавить необходимые настройки.

В нашем файле server.js давайте импортируем утилиту Node Path вверху файла, так как мы используем ее в нашем коде, чтобы указать путь к нашим представлениям.

 const Path = require('path'); 

Теперь замените server.route(Routes); строка со следующим блоком кода:

 server.register([ require('vision') ], (err) => { Hoek.assert(!err, err); // View settings server.views({ engines: { pug: require('pug') }, path: Path.join(__dirname, 'lib/views'), compileOptions: { pretty: false }, isCached: Settings.env === 'production' }); // Add routes server.route(Routes); }); 

В коде, который мы добавили, мы сначала регистрируем плагин Vision на нашем сервере Hapi.js, который будет обеспечивать функциональность представления. Затем мы добавляем настройки для наших представлений — например, движок, который мы собираемся использовать, и путь, где расположены представления. В конце блока кода мы снова добавляем наши маршруты.

Это заставит работать наши представления на сервере, но мы все равно должны объявить представление, которое мы будем использовать для каждого маршрута.

Настройка домашнего вида

Откройте файл lib/controllers/home.js и замените reply(result); строка со следующим:

 reply.view('home', { data: { notes: result }, page: 'Home—Notes Board', description: 'Welcome to my Notes Board' }); 

После регистрации плагина Vision у нас теперь есть метод view доступный для объекта ответа, и мы собираемся использовать его для выбора home представления в каталоге наших views и для отправки данных, которые будут использоваться при визуализации представлений.

В данные, которые мы предоставляем представлению, мы также включаем заголовок страницы и мета-описание для поисковых систем.

Настройка вида заметки: функция создания

Прямо сейчас, каждый раз, когда мы создаем заметку, мы получаем объект JSON с сервера на клиент. Но так как мы делаем этот процесс с Ajax, мы можем отправить новую заметку в виде HTML, готовую для добавления на страницу. Для этого мы визуализируем компонент заметки с имеющимися у нас данными. Заменить строку reply(result); со следующим блоком кода:

 // Generate a new note with the 'result' data const newNote = Pug.renderFile( Path.join(__dirname, '../views/components/note.pug'), { note: result } ); reply(newNote); 

Мы используем метод renderFile из Pug для визуализации шаблона заметки с данными, которые мы только что получили из нашей модели.

Настройка вида заметки: функция чтения

Когда мы заходим на страницу заметки, мы должны получить шаблон заметки с содержанием нашей заметки. Для этого мы должны заменить reply(result); строка с этим:

 reply.view('note', { note: result, page: `${result.title}—Notes Board`, description: result.description }); 

Как и на домашней странице, мы выбираем представление в качестве первого параметра и данные, которые мы собираемся использовать в качестве второго.

Настройка вида заметки: функция обновления

Каждый раз, когда мы обновляем заметку, мы отвечаем аналогично тому, как мы создаем новые заметки. Заменить reply(result); строка со следующим кодом:

 // Generate a new note with the updated data const updatedNote = Pug.renderFile( Path.join(__dirname, '../views/components/note.pug'), { note: result } ); reply(updatedNote); 

Примечание: функция удаления не нуждается в просмотре, поскольку она просто перенаправит на домашнюю страницу после удаления заметки.

Обслуживание статических файлов

Файлы JavaScript и CSS, которые мы используем на стороне клиента, предоставляются Hapi.js из каталога static/public/ . Но это не произойдет автоматически; мы должны указать серверу, что мы хотим определить эту папку как общедоступную. Это делается с помощью пакета Inert , который вы можете установить с помощью следующей команды:

 npm install -s inert 

В функции server.register внутри файла server.js импортируйте плагин server.js и зарегистрируйте его в Hapi следующим образом:

 server.register([ require('vision'), require('inert') ], (err) => { , server.register([ require('vision'), require('inert') ], (err) => { 

Теперь нам нужно определить маршрут, где мы будем предоставлять статические файлы, и их расположение в файловой системе нашего сервера. Добавьте следующую запись в конце экспортируемого объекта routes.js :

 { // Static files method: 'GET', path: '/{param*}', handler: { directory: { path: Path.join(__dirname, '../static/public') } }, config: { description: 'Provides static resources' } } 

Этот маршрут будет использовать метод GET и мы заменили функцию-обработчик объектом, содержащим каталог, который мы хотим сделать общедоступным.

Вы можете найти больше информации об обслуживании статического контента в документации Hapi.js.

Примечание. Проверьте Github-репозиторий на наличие остальных статических файлов , например основной таблицы стилей .

Вывод

На данный момент у нас есть очень простое приложение Hapi.js, использующее архитектуру MVC. Хотя есть еще вещи, о которых мы должны позаботиться, прежде чем запускать наше приложение в производство (например, проверка ввода, обработка ошибок, страницы ошибок и т. Д.), Это должно послужить основой для изучения и создания ваших собственных приложений.

Если вы хотите продолжить этот пример, после завершения всех мелких деталей (не связанных с архитектурой), чтобы сделать это надежное приложение, вы можете внедрить систему аутентификации, чтобы только зарегистрированные пользователи могли публиковать и редактировать заметки. Но ваше воображение — это предел, поэтому не стесняйтесь раскошелиться на репозиторий приложений , поэкспериментируйте с вашими идеями и дайте мне знать, что вы создаете в комментариях ниже!

Эта статья была рецензирована Марком Брауном и Вилданом Софтиком . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!