Если вы разработчик, который в настоящее время использует такие фреймворки, как Django, Laravel или Rails, вы, вероятно, слышали о Node.js. Возможно, вы уже используете популярную интерфейсную библиотеку, такую как Angular или React, в своих проектах. К настоящему времени вы должны подумать о полном переходе на серверную технологию на основе Node.js.
Однако большой вопрос в том, с чего начать. Сегодня мир JavaScript растет невероятно быстрыми темпами в последние несколько лет, и, похоже, он постоянно расширяется.
Если вы боитесь потерять свой с трудом заработанный опыт программирования во вселенной Node, не бойтесь, так как у нас есть Sails.js .
Sails.js — это среда MVC реального времени, предназначенная для того, чтобы помочь разработчикам в короткие сроки создавать готовые к выпуску приложения Node.js корпоративного уровня. Sails.js — это чистое решение JavaScript, которое поддерживает несколько баз данных (одновременно) и несколько интерфейсных технологий. Если вы разработчик Rails, вы будете рады узнать, что Майк Макнил , основатель Sails.js, был вдохновлен Rails. Вы найдете много общего между Rails и Sails.js.
В этой статье я научу вас основам Sails.js и покажу, как создать простое и удобное приложение для чата. Полный исходный код проекта sails-chat можно найти в этом репозитории GitHub .
Предпосылки
Прежде чем начать, вам нужно как минимум иметь опыт разработки приложений с использованием архитектуры MVC. Этот учебник предназначен для разработчиков среднего уровня. Вам также понадобится хотя бы базовая основа в этих вопросах:
Чтобы сделать его практичным и справедливым для всех, в этом руководстве будут использованы основные библиотеки, установленные по умолчанию в новом проекте Sails.js. Интеграция с современными интерфейсными библиотеками, такими как React, Vue или Angular, здесь не рассматривается. Тем не менее, я настоятельно рекомендую вам ознакомиться с ними после этой статьи. Кроме того, мы не будем делать интеграции с базой данных. Вместо этого мы будем использовать стандартную локальную дисковую файловую базу данных для разработки и тестирования.
План проэкта
Цель этого руководства — показать, как создать приложение для чата, похожее на Slack , Gitter или Discord .
На самом деле, нет! Много времени и пота ушло на строительство этих замечательных платформ. Текущее количество функций, разработанных в них, довольно огромно.
Вместо этого мы создадим минимальную жизнеспособную версию продукта приложения чата, которая состоит из:
- единый чат
- обычная аутентификация (без пароля)
- обновление профиля.
Я добавил функцию профиля в качестве бонуса, чтобы немного расширить возможности Sails.js.
Установка Sails.js
Прежде чем мы начнем установку Sails.js, нам нужно сначала настроить правильную среду Node.js. На момент написания последней доступной стабильной версии v0.12.14. Sails.js v1.0.0 также доступен, но в настоящее время находится в бета-версии, не рекомендуется для производственного использования.
Последняя стабильная версия Node, к которой у меня есть доступ, v8.9.4. К сожалению, Sails.js v0.12 не работает должным образом с текущей последней LTS. Тем не менее, я протестировал с Node v.7.10 и обнаружил, что все работает гладко. Это все еще хорошо, так как мы можем использовать новый синтаксис ES8 в нашем коде.
Как разработчик JavaScript, вы поймете, что работы с одной версией Node.js недостаточно. Поэтому я рекомендую использовать инструмент nvm для простого управления несколькими версиями Node.js и NPM. Если вы этого не сделали, просто очистите существующую установку Node.js, а затем установите nvm, чтобы помочь вам управлять несколькими версиями Node.js.
Вот основные инструкции по установке Node v7 и Sails.js:
# Install the latest version of Node v7 LTS nvm install v7 # Make Node v7 the default nvm default alias v7 # Install Sails.js Global npm install -g sails
Если у вас хорошее интернет-соединение, это займет всего пару минут или меньше. Теперь давайте создадим наше новое приложение с помощью команды генератора Sails:
# Go to your projects folder cd Projects # Generate your new app sails generate new chat-app # Wait for the install to finish then navigate to the project folder cd chat-app # Start the app sails lift
Запуск приложения должен занять несколько секунд. Вам нужно вручную открыть URL-адрес http://localhost:1337
в браузере, чтобы увидеть только что созданное веб-приложение.
Это подтверждает, что у нас есть работающий проект без ошибок, и мы можем начать работать. Чтобы остановить проект, просто нажмите control + c на терминале. Используйте свой любимый редактор кода (я использую Atom), чтобы изучить сгенерированную структуру проекта. Ниже приведены основные папки, о которых вы должны знать:
-
api
: контроллеры, модели, сервисы и политики (разрешения) -
assets
: изображения, шрифты, JS, CSS, Less, Sass и т. д. -
config
: конфигурация проекта, например, база данных, маршруты, учетные данные, локали, безопасность и т. д. -
node_modules
: установленные пакеты npm -
tasks
: скрипты конфигурации Grunt и конвейерный скрипт для компиляции и внедрения ресурсов - Представления: просмотр страниц — например, EJS, Jade или любой другой шаблонизатор, который вы предпочитаете
-
.tmp
: временная папка, используемая Sails для создания и обслуживания вашего проекта в режиме разработки.
Прежде чем мы продолжим, нужно сделать пару вещей:
- Обновите пакет EJS . Если у вас есть пакет EJS 2.3.4, указанный в
package.json
, вам необходимо обновить его, немедленно изменив его на 2.5.5. Он содержит серьезную уязвимость безопасности. После изменения номера версии выполните установку npm, чтобы выполнить обновление. - Горячая перезагрузка . Я предлагаю вам установить sails-hook-autoreload, чтобы включить горячую перезагрузку для вашего приложения Sails.js. Это не идеальное решение, но облегчит разработку. Чтобы установить его для этой текущей версии Sails.js, выполните следующее:
npm install [email protected] --save
Установка внешних зависимостей
В этом уроке мы потратим как можно меньше времени на создание пользовательского интерфейса. Подойдет любой CSS-фреймворк. В этом уроке я пойду с библиотекой CSS Semantic UI .
У Sails.js нет специального руководства по установке библиотек CSS. Есть три или более способов сделать это. Давайте посмотрим на каждого.
1. Ручная загрузка
Вы можете загрузить файлы CSS и JS-скрипты самостоятельно, вместе с их зависимостями. После загрузки поместите файлы в папку assets
.
Я предпочитаю не использовать этот метод,
поскольку это требует ручного усилия, чтобы держать файлы обновленными. Мне нравится автоматизировать задачи.
2. Использование Bower
Этот метод требует, чтобы вы создали файл с именем .bowerrc
в корне вашего проекта. Вставьте следующий фрагмент:
{ "directory" : "assets/vendor" }
Это даст Bower команду установить в папку assets/vendor
вместо папки по умолчанию bower_components
. Далее, установите Bower глобально, а ваши внешние интерфейсы локально, используя Bower:
# Install bower globally via npm- npm install -g bower # Create bower.json file, accept default answers (except choose y for private) bower init # Install semantic-ui via bower bower install semantic-ui --save # Install jsrender bower install jsrender --save
Я объясню цель jsrender
позже. Я думал, что лучше всего завершить задачу установки зависимостей за один раз. Вы должны принять к сведению, что jQuery также установлен, так как это зависимость от semantic-ui
.
После установки обновите assets/style/importer.less
следующую строку:
@import '../vendor/semantic/dist/semantic.css';
Далее включите JavaScript-зависимости в tasks/pipeline.js
:
var jsFilesToInject = [ // Load Sails.io before everything else 'js/dependencies/sails.io.js', // Vendor dependencies 'vendor/jquery/dist/jquery.js', 'vendor/semantic/dist/semantic.js', 'vendor/jsrender/jsrender.js', // Dependencies like jQuery or Angular are brought in here 'js/dependencies/**/*.js', // All of the rest of your client-side JS files // will be injected here in no particular order. 'js/**/*.js' ];
Когда мы запускаем sails lift
, файлы JavaScript будут автоматически views/layout.ejs
файл views/layout.ejs
в соответствии с инструкциями views/layout.ejs
Текущая настройка grunt
позаботится о внедрении наших CSS-зависимостей для нас.
Важно: добавьте слово vendor
в файл .gitignore
. Мы не хотим, чтобы зависимости от поставщиков сохранялись в нашем хранилище.
3. Используя npm + grunt.copy
Третий метод требует немного больше усилий для настройки, но приведет к снижению занимаемой площади. Установите зависимости, используя npm, следующим образом:
npm install semantic-ui-css jsrender --save
jQuery будет установлен автоматически, так как он также указан как зависимость для semantic-ui-css
. Далее нам нужно поместить код в tasks/config/copy.js
Этот код проинструктирует Grunt скопировать для нас необходимые файлы JS и CSS из node_modules
в папку assets/vendor
. Весь файл должен выглядеть так:
module.exports = function(grunt) { grunt.config.set('copy', { dev: { files: [{ expand: true, cwd: './assets', src: ['**/*.!(coffee|less)'], dest: '.tmp/public' }, //Copy JQuery { expand: true, cwd: './node_modules/jquery/dist/', src: ['jquery.min.js'], dest: './assets/vendor/jquery' }, //Copy jsrender { expand: true, cwd: './node_modules/jsrender/', src: ['jsrender.js'], dest: './assets/vendor/jsrender' }, // copy semantic-ui CSS and JS files { expand: true, cwd: './node_modules/semantic-ui-css/', src: ['semantic.css', 'semantic.js'], dest: './assets/vendor/semantic-ui' }, //copy semantic-ui icon fonts { expand: true, cwd: './node_modules/semantic-ui-css/themes', src: ["*.*", "**/*.*"], dest: './assets/vendor/semantic-ui/themes' }] }, build: { files: [{ expand: true, cwd: '.tmp/public', src: ['**/*'], dest: 'www' }] } }); grunt.loadNpmTasks('grunt-contrib-copy'); };
Добавьте эту строку в assets/styles/importer.less
:
@import '../vendor/semantic-ui/semantic.css';
Добавьте файлы JS в config/pipeline.js
:
// Vendor Dependencies 'vendor/jquery/jquery.min.js', 'vendor/semantic-ui/semantic.js', 'vendor/jsrender/jsrender.js',
Наконец, выполните эту команду, чтобы скопировать файлы из node_modules
assets/vendor
. Вам нужно сделать это только один раз для каждой чистой установки вашего проекта:
grunt copy:dev
Не забудьте добавить vendor
в свой .gitignore
.
Установка зависимостей тестирования
Какой бы метод вы ни выбрали, вам нужно убедиться, что загружаются необходимые зависимости. Для этого замените код в view/homepage.ejs
следующим:
<h2 class="ui icon header"> <i class="settings icon"></i> <div class="content"> Account Settings <div class="sub header">Manage your account settings and set e-mail preferences.</div> </div> </h2>
После сохранения файла сделайте sails lift
. Ваша домашняя страница должна теперь выглядеть так:
Всегда делайте обновление после перезапуска вашего приложения. Если значок отсутствует или шрифт выглядит не так, пожалуйста, внимательно просмотрите шаги и посмотрите, что вы пропустили. Используйте консоль браузера, чтобы увидеть, какие файлы не загружаются. В противном случае перейдите к следующему этапу.
Создание видов
Когда дело доходит до разработки проекта, мне нравится начинать с пользовательского интерфейса. Мы будем использовать встроенный шаблон JavaScript для создания представлений. Это шаблонизатор, установленный по умолчанию в каждом проекте Sails.js. Однако вы должны знать, что он имеет ограниченную функциональность и больше не разрабатывается.
Откройте config/bootstrap.js
и вставьте эту строку, чтобы дать правильное название нашим веб-страницам. Поместите его прямо в существующую функцию перед оператором cb()
:
sails.config.appName = "Sails Chat App";
Вы можете взглянуть на views/layout.ejs
чтобы увидеть, как установлен тег title
. Далее мы начинаем создавать интерфейс домашней страницы.
Дизайн домашней страницы
Откройте /views/homepage.ejs
и замените существующий код следующим:
<div class="banner"> <div class="ui segment teal inverted"> <h1 class="ui center aligned icon header"> <i class="chat icon"></i> <div class="content"> <a href="/">Sails Chat</a> <div class="sub header">Discuss your favorite technology with the community!</div> </div> </h1> </div> </div> <div class="section"> <div class="ui three column grid"> <div class="column"></div> <div class="column"> <div class="ui centered padded compact raised segment"> <h3>Sign Up or Sign In</h3> <div class="ui divider"></div> [TODO : Login Form goes here] </div> </div> <div class="column"></div> </div> </div>
Чтобы понять элементы пользовательского интерфейса, используемые в приведенном выше коде, обратитесь к документации по семантическому интерфейсу. Я обрисовал точные ссылки ниже:
Создайте новый файл в assets/styles/theme.less
и вставьте следующее содержимое:
.banner a { color: #fff; } .centered { margin-left: auto !important; margin-right: auto !important; margin-bottom: 30px !important; } .section { margin-top: 30px; } .menu { border-radius: 0 !important; } .note { font-size: 11px; color: #2185D0; } #chat-content { height: 90%; overflow-y: scroll; }
Это все пользовательские стили, которые мы будем использовать в нашем проекте. Остальная часть стиля будет исходить из библиотеки Semantic UI
.
Затем обновите assets/styles/importer.less
чтобы включить файл темы, который мы только что создали:
@import 'theme.less';
Выполнить sails lift
. Ваш проект должен теперь выглядеть так:
Далее мы рассмотрим построение меню навигации.
Меню навигации
Это будет создано как частичное, так как оно будет совместно использоваться несколькими файлами просмотра. Внутри папки views
создайте папку с именем partials
. Затем создайте файл views/partials/menu.ejs
и вставьте следующий код:
<div class="ui labeled icon inverted teal menu"> <a class="item" href="/chat"> <i class="chat icon"></i> Chat Room </a> <a class="item" href="/profile"> <i class="user icon"></i> Profile </a> <div class="right menu"> <a class="item" href="/auth/logout"> <i class="sign out icon"></i> Logout </a> </div> </div>
Чтобы понять приведенный выше код, просто обратитесь к документации меню .
Если вы посмотрите приведенный выше код, вы заметите, что мы создали ссылку для /chat
, /profile
и /auth/logout
. Давайте сначала создадим представления для profile
и chat room
.
Профиль
Создайте файл view/profile.ejs
и вставьте следующий код:
<% include partials/menu %> <div class="ui container"> <h1 class="ui centered header">Profile Updated!</h1> <hr> <div class="section"> [ TODO put user-form here] </div> </div>
К настоящему времени вы должны быть знакомы с элементами интерфейса header
и grid
если вы читали связанную документацию. В корне документа вы заметите, что у нас есть элемент container
. (Узнайте больше об этом в документации контейнера .
Мы создадим пользовательскую форму позже, как только мы создадим API. Далее мы создадим макет для чата.
План комнаты чата
Комната чата будет состоять из трех разделов:
- Пользователи чата — список пользователей
- Сообщения чата — список сообщений
- Сообщение чата — форма для размещения новых сообщений.
Создайте views/chatroom.ejs
и вставьте следующий код:
<% include partials/menu %> <div class="chat-section"> <div class="ui container grid"> <!-- Members List Section --> <div class="four wide column"> [ TODO chat-users ] </div> <div class="twelve wide column"> <!-- Chat Messages --> [ TODO chat-messages ] <hr> <!-- Chat Post --> [ TODO chat-post ] </div> </div> </div>
Прежде чем мы сможем просматривать страницы, нам нужно настроить маршрутизацию.
Маршрутизация
Откройте config/routes.js
и обновите его следующим образом:
'/': { view: 'homepage' }, '/profile': { view: 'profile' }, '/chat': { view: 'chatroom' }
Маршрутизация Sails.js довольно гибкая. Существует много способов определения маршрутизации в зависимости от сценария. Это самая простая версия, где мы сопоставляем URL-адрес с представлением.
Запустите приложение Sails или просто обновите страницу, если она все еще работает в фоновом режиме. В настоящее время нет ссылки между домашней страницей и другими страницами. Это сделано намеренно, поскольку позже мы создадим элементарную систему аутентификации, которая будет перенаправлять зарегистрированных пользователей в /chat
. Сейчас используйте адресную строку вашего браузера и добавьте /chat
или /profile
в конце URL.
На этом этапе вы должны иметь вышеуказанные мнения. Давайте продолжим и начнем создавать API.
Генерация пользовательского API
Мы будем использовать утилиту командной строки Sails.js для генерации нашего API. Нам нужно остановить приложение для этого шага:
sails generate api User
Через секунду мы получаем сообщение «Создан новый API!». В основном, для нас только что были созданы модель User.js
и UserController.js
. Давайте обновим api/model/User.js
некоторыми атрибутами модели:
module.exports = { attributes: { name: { type: 'string', required: true }, email: { type: 'string', required: true, unique: true }, avatar: { type: 'string', required: true, defaultsTo: 'https://s.gravatar.com/avatar/e28f6f64608c970c663197d7fe1f5a59?s=60' }, location: { type: 'string', required: false, defaultsTo: '' }, bio: { type: 'string', required: false, defaultsTo:'' } } };
Я считаю, что приведенный выше код не требует пояснений. По умолчанию Sails.js использует локальную дисковую базу данных, которая в основном представляет собой файл, расположенный в папке .tmp
. Чтобы протестировать наше приложение, нам нужно создать несколько пользователей. Самый простой способ сделать это — установить пакет sails-seed :
npm install sails-seed --save
После установки вы обнаружите, что файл config/seeds.js
был создан для вас. Вставьте следующие начальные данные:
module.exports.seeds = { user: [ { name: 'John Wayne', email: '[email protected]', avatar: 'https://randomuser.me/api/portraits/men/83.jpg', location: 'Mombasa', bio: 'Spends most of my time at the beach' }, { name: 'Peter Quinn', email: '[email protected]', avatar: 'https://randomuser.me/api/portraits/men/32.jpg', location: 'Langley', bio: 'Rather not say' }, { name: 'Jane Eyre', email: '[email protected]', avatar: 'https://randomuser.me/api/portraits/women/94.jpg', location: 'London', bio: 'Loves reading motivation books' } ] }
Теперь, когда мы создали API, мы должны настроить политику миграции в файле config/models.js
:
migrate: 'drop'
Существует три стратегии миграции, которые Sails.js использует для определения того, как перестраивать вашу базу данных при каждом запуске:
- безопасно — не мигрируй, я сделаю это вручную
- изменить — перенести, но попытаться сохранить существующие данные
- drop — отбросить все таблицы и восстановить все
Я предпочитаю использовать drop
для разработки, так как я часто повторяю. Вы можете установить alter
если хотите сохранить существующие данные. Тем не менее, наша база данных будет заполняться начальными данными каждый раз.
Теперь позвольте мне показать вам кое-что классное. Запустите ваш проект Sails и перейдите по адресам /user
и /user/1
.
Благодаря API Sails.js Blueprints у нас есть полнофункциональный CRUD API, и мы не пишем ни единой строки кода. Вы можете использовать Postman для доступа к пользовательскому API и выполнять манипуляции с данными, такие как создание, обновление или удаление пользователей.
Давайте теперь приступим к построению формы профиля.
Анкета
Откройте view/profile.ejs
и замените существующую строку TODO следующим кодом:
<img class="ui small centered circular image" src="<%= data.avatar %>"> <div class="ui grid"> <form action="<%= '/user/update/'+ data.id %>" method="post" class="ui centered form"> <div class="field"> <label>Name</label> <input type="text" name="name" value="<%= data.name %>"> </div> <div class="field"> <label>Email</label> <input type="text" name="email" value="<%= data.email %>"> </div> <div class="field"> <label>Location</label> <input type="text" name="location" value="<%= data.location %>"> </div> <div class="field"> <label>Bio</label> <textarea name="bio" rows="4" cols="40"><%= data.bio %></textarea> </div> <input type="hidden" name="avatar" value=<%=data.avatar %>> <button class="ui right floated orange button" type="submit">Update</button> </form> </div>
Мы используем Semantic-UI Form для создания интерфейса формы. Если вы изучите значение действия формы /user/update/'+ data.id
, вы поймете, что я использую маршрут Blueprint. Это означает, что когда пользователь нажимает кнопку « Update
, выполняется действие по обновлению Blueprint.
Однако для загрузки пользовательских данных я решил определить пользовательское действие в пользовательском контроллере. Обновите api/controllers/UserController
следующим кодом:
module.exports = { render: async (request, response) => { try { let data = await User.findOne({ email: '[email protected]' }); if (!data) { return response.notFound('The user was NOT found!'); } response.view('profile', { data }); } catch (err) { response.serverError(err); } } };
В этом коде вы заметите, что я использую синтаксис async/await
для извлечения пользовательских данных из базы данных. Альтернативой является использование обратных вызовов, которые для большинства разработчиков не совсем понятны. Я также жестко запрограммировал учетную запись пользователя по умолчанию для временной загрузки. Позже, когда мы настроим базовую аутентификацию, мы изменим ее, чтобы загрузить текущего зарегистрированного пользователя.
Наконец, нам нужно изменить маршрут /profile
чтобы начать использовать только что созданный UserController
. Откройте config/routes
и обновите профиль маршрута следующим образом:
... '/profile': { controller: 'UserController', action: 'render' }, ...
Перейдите к URL /profile
, и у вас должно появиться следующее представление:
Попробуйте изменить одно из полей формы и нажмите кнопку обновления. Вы попадете в это представление:
Вы заметите, что обновление сработало, но отображаемые данные представлены в формате JSON. В идеале у нас должна быть страница профиля только для views/user/findOne.ejs
в views/user/findOne.ejs
и страница профиля обновления в /views/user/update.ejs
. Система Blueprint будет угадывать виды, используемые для визуализации информации. Если он не может найти представления, он просто выведет JSON. Сейчас мы просто воспользуемся этим изящным приемом. Создайте файл /views/user/update.ejs
и вставьте следующий код:
<script type="text/javascript"> window.location = '/profile'; </script>
В следующий раз, когда мы выполним обновление, мы будем перенаправлены на страницу /profile
. Теперь, когда у нас есть пользовательские данные, мы можем создать файл views/partials/chat-users.js
для использования в views/chatroom.ejs
. После того, как вы создали файл, вставьте этот код:
<div class="ui basic segment"> <h3>Members</h3> <hr> <div id="users-content" class="ui middle aligned selection list"> </div> </div> // jsrender template <script id="usersTemplate" type="text/x-jsrender"> <div class="item"> <img class="ui avatar image" src="{{:avatar}}"> <div class="content"> <div class="header">{{:name}}</div> </div> </div> </script> <script type="text/javascript"> function loadUsers() { // Load existing users io.socket.get('/user', function(users, response) { renderChatUsers(users); }); // Listen for new & updated users io.socket.on('user', function(body) { io.socket.get('/user', function(users, response) { renderChatUsers(users); }); }); } function renderChatUsers(data) { const template = $.templates('#usersTemplate'); let htmlOutput = template.render(data); $('#users-content').html(htmlOutput); } </script>
Для этого представления нам нужен подход на стороне клиента для обновления страницы в режиме реального времени. Здесь мы используем библиотеку jsrender , более мощный шаблонизатор, чем EJS. Прелесть jsrender
том, что он может принимать массив или один литерал объекта, и шаблон все равно будет отображаться правильно. Если бы мы делали это в ejs
, нам нужно было бы объединить оператор if
и цикл for
для обработки обоих случаев.
Позвольте мне объяснить поток нашего клиентского JavaScript-кода:
-
loadUsers()
. Когда страница загружается впервые, мы используем библиотеку сокетов Sails.js для выполнения запросаGET
для пользователей. Этот запрос будет обработан Blueprint API. Затем мы передаем полученные данные вrenderChatUsers(data)
. - В функции
loadUsers()
мы регистрируем слушателя сio.socket.on
функцииio.socket.on
. Мы слушаем события, относящиеся кuser
модели. Когда мы получаем уведомление, мы снова выбираем пользователей и заменяем существующий вывод HTML. -
renderChatUsers(data)
. Здесь мы получаем скрипт с идентификаторомusersTemplate
с помощью функции jQuerytemplates()
. Обратите внимание на типtext/x-jsrender
. Указав пользовательский тип, браузер будет игнорировать и пропускать этот раздел, поскольку он не знает, что это такое. Затем мы используем функциюtemplate.render()
для объединения шаблона с данными. Этот процесс сгенерирует вывод HTML, который мы затем возьмем и вставим в документ HTML.
Шаблон, который мы написали в profile.ejs
был обработан на сервере Node, а затем отправлен в браузер в формате HTML. Для chat-users
нам нужно выполнить рендеринг на стороне клиента. Это позволит пользователям чата видеть новых пользователей, присоединяющихся к группе, без обновления своего браузера.
Прежде чем мы протестируем код, нам нужно обновить views/chatroom.ejs
чтобы включить только что созданную часть chat-users
. Замените [ TODO chat-users ]
на этот код:
...html <% include partials/chat-users.ejs %> ...
В том же файле добавьте этот скрипт в конце:
<script type="text/javascript"> window.onload = function() { loadUsers(); } </script>
Этот скрипт вызовет loadUsers()
. Чтобы убедиться, что это работает, давайте выполним sails lift
и перейдем к URL /chat
.
Ваше мнение должно понравиться, как на картинке выше. Если это произойдет, давайте приступим к созданию API чата.
ChatMessage API
Как и прежде, мы будем использовать Sails.js для генерации API:
sails generate api ChatMessage
Далее, заполните api/models/ChatMessage.js
этими атрибутами:
module.exports = { attributes: { message: { type: 'string', required: true }, createdBy : { model: 'user', required: true } } };
Обратите внимание, что мы объявили взаимно-однозначную связь с моделью User
через атрибут createdBy
. Далее нам нужно заполнить нашу дисковую базу несколькими сообщениями чата. Для этого мы будем использовать config/bootstrap.js
. Обновите весь код следующим образом. Мы используем синтаксис async/await
чтобы упростить наш код и избежать ада обратного вызова:
module.exports.bootstrap = async function(cb) { sails.config.appName = "Sails Chat App"; // Generate Chat Messages try { let messageCount = ChatMessage.count(); if(messageCount > 0){ return; // don't repeat messages } let users = await User.find(); if(users.length >= 3) { console.log("Generating messages...") let msg1 = await ChatMessage.create({ message: 'Hey Everyone! Welcome to the community!', createdBy: users[1] }); console.log("Created Chat Message: " + msg1.id); let msg2 = await ChatMessage.create({ message: "How's it going?", createdBy: users[2] }); console.log("Created Chat Message: " + msg2.id); let msg3 = await ChatMessage.create({ message: 'Super excited!', createdBy: users[0] }); console.log("Created Chat Message: " + msg3.id); } else { console.log('skipping message generation'); } }catch(err){ console.error(err); } // It's very important to trigger this callback method when you're finished with Bootstrap! (Otherwise your server will never lift, since it's waiting on Bootstrap) cb(); };
Самое замечательное, что генератор семян запускается до bootstrap.js
. Таким образом, мы уверены, что данные о Users
были созданы первыми, чтобы мы могли использовать их для заполнения поля createdBy
. Наличие тестовых данных позволит нам быстро выполнять итерации при создании пользовательского интерфейса.
Пользовательский интерфейс сообщений чата
Идите дальше и создайте новый файл views/partials/chat-messages.ejs
, затем разместите этот код:
<div class="ui basic segment" style="height: 70vh;"> <h3>Community Conversations</h3> <hr> <div id="chat-content" class="ui feed"> </div> </div> <script id="chatTemplate" type="text/x-jsrender"> <div class="event"> <div class="label"> <img src="{{:createdBy.avatar}}"> </div> <div class="content"> <div class="summary"> <a href="#"> {{:createdBy.name}}</a> posted on <div class="date"> {{:createdAt}} </div> </div> <div class="extra text"> {{:message}} </div> </div> </div> </script> <script type="text/javascript"> function loadMessages() { // Load existing chat messages io.socket.get('/chatMessage', function(messages, response) { renderChatMessages(messages); }); // Listen for new chat messages io.socket.on('chatmessage', function(body) { renderChatMessages(body.data); }); } function renderChatMessages(data) { const chatContent = $('#chat-content'); const template = $.templates('#chatTemplate'); let htmlOutput = template.render(data); chatContent.append(htmlOutput); // automatically scroll downwards const scrollHeight = chatContent.prop("scrollHeight"); chatContent.animate({ scrollTop: scrollHeight }, "slow"); } </script>
Логика здесь очень похожа на chat-users
. В разделе прослушивания есть одно ключевое отличие. Вместо того, чтобы заменить вывод, мы используем append. Затем мы делаем анимацию прокрутки внизу списка, чтобы пользователи увидели новое входящее сообщение.
Далее, давайте обновим chatroom.ejs
включив в него новую часть chat-messages
а также обновим скрипт для вызова функции loadMessages()
:
... <!-- Chat Messages --> <% include partials/chat-messages.ejs %> ... <script type="text/javascript"> ... loadMessages(); ... </script>
Ваше мнение должно теперь выглядеть так:
Давайте теперь создадим простую форму, которая позволит пользователям публиковать сообщения в чате.
Пользовательский интерфейс сообщения чата
Создайте новый файл views/partial/chat-post.ejs
и вставьте этот код:
<div class="ui basic segment"> <div class="ui form"> <div class="ui field"> <label>Post Message</label> <textarea id="post-field" rows="2"></textarea> </div> <button id="post-btn" class="ui right floated large orange button" type="submit">Post</button> </div> <div id="post-err" class="ui tiny compact negative message" style="display:none;"> <p>Oops! Something went wrong.</p> </div> </div>
Здесь мы используем элементы semantic-ui
для построения формы. Затем добавьте этот скрипт в конец файла:
<script type="text/javascript"> function activateChat() { const postField = $('#post-field'); const postButton = $('#post-btn'); const postErr = $('#post-err'); // Bind to click event postButton.click(postMessage); // Bind to enter key event postField.keypress(function(e) { var keycode = (e.keyCode ? e.keyCode : e.which); if (keycode == '13') { postMessage(); } }); function postMessage() { if(postField.val() == "") { alert("Please type a message!"); } else { let text = postField.val(); io.socket.post('/postMessage', { message: text }, function(resData, jwRes) { if(jwRes.statusCode != 200) { postErr.html("<p>" + resData.message +"</p>") postErr.show(); } else { postField.val(''); // clear input field } }); } } } </script>
Этот скрипт состоит из двух функций:
-
activateChat()
. Эта функция привязывает кнопку публикации к событию щелчка, а окно сообщения (поле сообщения) — к событию нажатия клавиши (ввод). Когда любой из них запускается, вызывается функцияpostMessage()
. -
postMessage
. Эта функция сначала выполняет быструю проверку, чтобы убедиться, что поле ввода после ввода не пустое. Если в поле ввода есть сообщение, мы используемio.socket.post()
для отправки сообщения обратно на сервер. Здесь мы используем классическую функцию обратного вызова для обработки ответа от сервера. Если возникает ошибка, мы отображаем сообщение об ошибке. Если мы получим код состояния 200, означающий, что сообщение было перехвачено, мы очищаем поле ввода сообщения, готовое для ввода следующего сообщения.
Если вы вернетесь к сценарию chat-message
, вы увидите, что мы уже разместили код для обнаружения и обработки входящих сообщений. Вы также должны были заметить, что io.socket.post()
отправляет данные на URL /postMessage
. Это не маршрут Blueprint, а пользовательский. Следовательно, нам нужно написать код для него.
Перейдите в api/controllers/UserController.js
и вставьте этот код:
module.exports = { postMessage: async (request, response) => { // Make sure this is a socket request (not traditional HTTP) if (!request.isSocket) { return response.badRequest(); } try { let user = await User.findOne({email:'[email protected]'}); let msg = await ChatMessage.create({message:request.body.message, createdBy:user }); if(!msg.id) { throw new Error('Message processing failed!'); } msg.createdBy = user; ChatMessage.publishCreate(msg); } catch(err) { return response.serverError(err); } return response.ok(); } };
Поскольку мы не настроили базовую аутентификацию, мы пока что johnnie86@gmail.com
пользователя johnnie86@gmail.com
как автора сообщения. Мы используем ORM-функцию Model.create()
Waterline для создания новой записи. Это причудливый способ вставки записей без написания кода SQL. Затем мы отправляем событие уведомления всем сокетам, информируя их о создании нового сообщения. Мы делаем это с помощью функции ChatMessage.publishCreate()
, которая определена в API Blueprints. Прежде чем отправить сообщение, мы убедимся, что поле createdBy
заполнено user
объектом. Это используется частичными chat-messages
для доступа к аватару и имени пользователя, создавшего сообщение.
Затем config/routes.js
к config/routes.js
чтобы сопоставить URL /postMessage
с действием postMessage
мы только что определили. Вставьте этот код:
... '/chat': { view: 'chatroom' }, // Add comma here '/postMessage': { controller: 'ChatMessageController', action: 'postMessage' } ...
Откройте views/chatroom.js
и chat-post
частичное chat-post
в chat-post
. Мы также вызовем функцию loadMessages()
сразу после функции loadMessages()
:
... <% include partials/chat-messages.ejs %> ... <script type="text/javascript"> ... activateChat(); ... </script>
Обновите страницу и попробуйте отправить несколько сообщений.
Теперь у вас должна быть функциональная система чата. Просмотрите исходный код проекта на случай, если вы застряли.
Базовая аутентификация
Настройка правильной системы аутентификации и авторизации выходит за рамки данного руководства. Итак, мы согласимся на базовую систему аутентификации без пароля. Давайте сначала создадим форму регистрации и авторизации.
Форма входа / регистрации
Создайте новый файл views/auth-form.ejs
и вставьте следующее содержимое:
<form method="post" action="/auth/authenticate" class="ui form"> <div class="field"> <label>Full Names</label> <input type="text" name="name" placeholder="Full Names" value="<%= typeof name != 'undefined' ? name : '' %>"> </div> <div class="required field"> <label>Email</label> <input type="email" name="email" placeholder="Email" value="<%= typeof email != 'undefined' ? email : '' %>"> </div> <button class="ui teal button" type="submit" name="action" value="signup">Sign Up & Login</button> <button class="ui blue button" type="submit" name="action" value="login">Login</button> <p class="note">*Provide email only for Login</p> </form> <% if(typeof error != 'undefined') { %> <div class="ui error message"> <div class="header"><%= error.title %></div> <p><%= error.message %></p> </div> <% } %>
Затем откройте views/homepage.ejs
и замените строку TODO следующим оператором включения:
... <% include partials/auth-form.ejs %> ...
Мы создали форму, которая позволяет вам создать новую учетную запись, указав имя и адрес электронной почты. Когда вы нажимаете « Signup & Login
войти», создается новая запись пользователя, и вы входите в систему. Однако, если электронная почта уже используется другим пользователем, появится сообщение об ошибке. Если вы просто хотите войти, просто укажите адрес электронной почты и нажмите кнопку « Login
. После успешной аутентификации вы будете перенаправлены на URL /chat
.
Прямо сейчас все, что я только что сказал, не работает. Нам нужно будет реализовать эту логику. Во-первых, давайте перейдем к /
address, чтобы подтвердить, что auth-form
выглядит товаром.
политика
Теперь, когда мы настраиваем систему аутентификации, нам нужно защитить /chat
и /profile
от публичного доступа. Только аутентифицированные пользователи должны иметь доступ к ним. Откройте config/policies.js
и вставьте этот код:
ChatMessageController: { '*': 'sessionAuth' }, UserController: { '*': 'sessionAuth' },
Указав имя контроллера, мы также эффективно заблокировали все маршруты, предоставляемые Blueprint API для пользователей и сообщениями чата. К сожалению, политики работают только с контроллерами. Это означает, что маршрут /chat
не могут быть защищены в его текущем состоянии. Нам нужно определить пользовательское действие для него. Откройте api/controller/ChatroomController.js
и вставьте этот код:
... render: (request, response) => { return response.view('chatroom'); },
Затем замените конфигурацию маршрута для /chat
на этот один config/routes.js
:
... '/chat': { controller: 'ChatMessageController', action: 'render' }, ...
Маршрут /chat
теперь должен быть защищен от публичного доступа. Если вы перезапустите свое приложение и попытаетесь получить доступ к /profile
, /chat
, /user
или /chatmessage
, вы получите следующее запрещенное сообщение:
Если вы хотите вместо этого перенаправить пользователей в форму входа, api/policies/sessionAuth
и замените запрещенный вызов следующим:
... // return res.forbidden('You are not permitted to perform this action.'); return res.redirect('/'); ...
Попробуйте снова получить доступ к запрещенным страницам, и вы автоматически будете перенаправлены на домашнюю страницу. Давайте теперь реализуем код регистрации и входа.
Auth Controller и Сервис
Сначала вам нужно остановить Sails.js, чтобы запустить эту команду:
sails generate controller Auth
Это создаст пустой api/controllers/AuthController
для нас. Откройте его и вставьте этот код:
authenticate: async (request, response) => { // Sign up user if(request.body.action == 'signup') { // Validate signup form // Check if email is registered // Create new user } // Log in user }, logout: (request, response) => { // Logout user }
Я разместил в комментариях, объясняющих, как будет протекать логика. Мы можем разместить соответствующий код здесь. Тем не менее, Sails.js рекомендует сохранять код нашего контроллера простым и понятным. Чтобы достичь этого, нам нужно написать вспомогательные функции, которые помогут нам в выполнении каждой из вышеперечисленных задач. Чтобы создать эти вспомогательные функции, нам нужно создать сервис. Сделайте это, создав новый файл api/services/AuthService.js
. Вставьте следующий код:
/** * AuthService.js * **/ const gravatar = require('gravatar') // Where to display auth errors const view = 'homepage'; module.exports = { sendAuthError: (response, title, message, options) => { options = options || {}; const { email, name} = options; response.view(view, { error: {title, message}, email, name }); return false; }, validateSignupForm: (request, response) => { if(request.body.name == '') { return AuthService.sendAuthError(response, 'Signup Failed!', "You must provide a name to sign up", {email:request.body.email}); } else if(request.body.email == '') { return AuthService.sendAuthError(response, 'Signup Failed!', "You must provide an email address to sign up", {name:request.body.name}); } return true; }, checkDuplicateRegistration: async (request, response) => { try { let existingUser = await User.findOne({email:request.body.email}); if(existingUser) { const options = {email:request.body.email, name:request.body.name}; return AuthService.sendAuthError(response, 'Duplicate Registration!', "The email provided has already been registered", options); } return true; } catch (err) { response.serverError(err); return false; } }, registerUser: async (data, response) => { try { const {name, email} = data; const avatar = gravatar.url(email, {s:200}, "https"); let newUser = await User.create({name, email, avatar}); // Let all sockets know a new user has been created User.publishCreate(newUser); return newUser; } catch (err) { response.serverError(err); return false; } }, login: async (request, response) => { try { let user = await User.findOne({email:request.body.email}); if(user) { // Login Passed request.session.userId = user.id; request.session.authenticated = true; return response.redirect('/chat'); } else { // Login Failed return AuthService.sendAuthError(response, 'Login Failed!', "The email provided is not registered", {email:request.body.email}); } } catch (err) { return response.serverError(err); } }, logout: (request, response) => { request.session.userId = null; request.session.authenticated = false; response.redirect('/'); } }
Изучите код внимательно. Как промежуточный разработчик, вы должны понимать логику. Я не сделал ничего необычного здесь. Тем не менее, я хотел бы упомянуть несколько вещей:
-
Gravatar. Вам необходимо установить Gravatar. Это библиотека JavaScript для генерации URL Gravatar на основе адреса электронной почты.
```bash npm install gravatar --save ```
-
User.publishCreate(newUser)
, Также какChatMessages
мы запускаем событие, уведомляющее все сокеты о том, что новый пользователь только что был создан. Это приведет к тому, что все вошедшие в систему клиенты будут повторно получать данные пользователей. Просмотрите, чтобы увидеть, о чем я говорю.views/partial/chat-users.js
-
request.session
, Sails.js предоставляет нам хранилище сеансов, которое мы можем использовать для передачи данных между запросами страниц. Сеанс Sails.js по умолчанию находится в памяти, что означает, что при остановке сервера данные сеанса теряются. ВAuthService
, мы используем сессию для храненияuserId
иauthenticated
статуса.
С логикой на месте, мы можем пойти дальше и обновить с помощью следующего кода:AuthService.js
api/controllers/AuthController
module.exports = { authenticate: async (request, response) => { const email = request.body.email; if(request.body.action == 'signup') { const name = request.body.name; // Validate signup form if(!AuthService.validateSignupForm(request, response)) { return; } // Check if email is registered const duplicateFound = await AuthService.checkDuplicateRegistration(request, response); if(!duplicateFound) { return; } // Create new user const newUser = await AuthService.registerUser({name,email}, response); if(!newUser) { return; } } // Attempt to log in const success = await AuthService.login(request, response); }, logout: (request, response) => { AuthService.logout(request, response); } };
Посмотрите, насколько прост и удобен для чтения наш контроллер. Далее давайте сделаем несколько последних штрихов.
Последние штрихи
Теперь, когда у нас настроена аутентификация, мы должны удалить жестко закодированное значение, которое мы поместили в postMessage
действие . Замените код электронной почты следующим:api/controllers/ChatMessageController
... let user = await User.findOne({id:request.session.userId}); ...
Я хотел бы упомянуть кое-что, что вы, возможно, не заметили, если вы посмотрите на URL выхода из системы , мы разместили этот адрес . Если вы посмотрите , вы заметите, что мы не разместили URL для него. Удивительно, но когда мы запускаем код, он работает. Это связано с тем, что Sails.js использует соглашение, чтобы определить, какой контроллер и какое действие необходимо выполнить для разрешения определенного адреса.views/partials/menu.ejs
/auth/logout
config/routes.js
К настоящему времени у вас должно быть функциональное приложение для чата MVP. Запустите ваше приложение и протестируйте следующие сценарии:
- зарегистрироваться, ничего не вводя
- зарегистрироваться, заполнив только имя
- зарегистрироваться, заполнив только электронную почту
- зарегистрироваться, указав имя и зарегистрированный адрес электронной почты — например, или
johnnie86@gmail.com
jane@hotmail.com
- зарегистрируйтесь используя ваше имя и адрес электронной почты
- Обновите профиль
- попробуйте опубликовать пустое сообщение
- опубликовать несколько сообщений
- open another browser and log in as another user, put each browser side by side and chat
- log out and create a new account.
Уф! That’s a lot of functionality we’ve just implemented in one sitting and then tested. With a few more weeks, we could whip out a production ready chat system integrated with more features, such as multiple chat rooms, channel attachments, smiley icons and social accounts integration!
Резюме
During this tutorial, we didn’t put the name of the logged-in user somewhere at the top menu. You should be capable of fixing this yourself. If you’ve read the entire tutorial, you should now be proficient in building applications using Sails.js.
Цель этого руководства — показать вам, что может быть из среды MVC, отличной от JavaScript, и создать что-то потрясающее с относительно небольшим количеством строк кода. Использование Blueprint API поможет вам быстрее реализовать функции. Я также рекомендую вам научиться интегрировать более мощную интерфейсную библиотеку, такую как React, Angular или Vue, для создания гораздо более интерактивного веб-приложения. Кроме того, обучение написанию тестов для Sails.js для автоматизации процесса тестирования является отличным оружием в вашем арсенале программирования.