В этой статье я расскажу вам о создании полнофункционального веб-приложения для голосования о Гарри Поттере в реальном времени.
Приложения реального времени обычно используют WebSockets, относительно новый тип протокола передачи, в отличие от HTTP, который является односторонним обменом данными, который происходит только тогда, когда пользователь запрашивает его. WebSockets обеспечивают постоянную связь между сервером и пользователем и всеми пользователями, подключенными к приложению, при условии, что соединение остается открытым.
Веб-приложение реального времени — это то, где информация передается (почти) мгновенно между пользователями и сервером (и, соответственно, между пользователями и другими пользователями). Это отличается от традиционных веб-приложений, где клиент должен запрашивать информацию с сервера. — Quora
Наше веб-приложение для голосования «Гарри Поттер» покажет варианты (все четыре дома) и диаграмму справа, которая обновляется, когда подключенный пользователь голосует.
Чтобы дать вам краткое представление о внешнем виде, окончательное приложение будет выглядеть так:
Вот небольшой предварительный просмотр работы приложения в реальном времени:
Чтобы сделать наше приложение в реальном времени, мы будем использовать Pusher и WebSockets. Pusher находится в режиме реального времени между вашими серверами и вашими клиентами. Он поддерживает постоянные соединения с клиентами — через WebSocket, если это возможно, и откат к HTTP-соединению — так что, как только ваши серверы получат новые данные для отправки клиентам, они смогут сделать это мгновенно через Pusher.
Создание нашего приложения
Давайте создадим наше новое приложение, используя команду npm init
. В интерактивном режиме вам зададут несколько вопросов о деталях вашего заявления. Вот что у меня было:
[email protected] ➜ Harry-Potter-Pusher $ npm init { "name": "harry-potter-pusher", "version": "1.0.0", "description": "A real-time voting application using Harry Potter's house selection for my article for Pusher.", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/praveenscience/Harry-Potter-Pusher.git" }, "keywords": [ "Harry_Potter", "Pusher", "Voting", "Real_Time", "Web_Application" ], "author": "license": "ISC", "bugs": { "url": "https://github.com/praveenscience/Harry-Potter-Pusher/issues" }, "homepage": "https://github.com/praveenscience/Harry-Potter-Pusher#readme" } Is this OK? (yes)
Итак, я оставил большинство настроек со значениями по умолчанию. Теперь пришло время установить зависимости.
Установка зависимостей
Нам нужны Express, body-parser, Cross Origin Resource Sharing (CORS), Mongoose и Pusher, установленные как зависимости. Чтобы установить все в одной команде, используйте следующее. Вы также можете увидеть, что выводит эта команда.
[email protected] ➜ Harry-Potter-Pusher $ npm i express body-parser cors pusher mongoose npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN [email protected] requires a peer of ajv@^6.0.0 but none is installed. You must install peer dependencies yourself. + [email protected] + [email protected] + [email protected] + [email protected] + [email protected] added 264 packages in 40.000s
Требуем наши модули
Поскольку это приложение Express, в первую очередь мы должны включить express()
. При этом нам также нужны некоторые сопутствующие модули. Итак, сначала давайте начнем с этого:
const express = require("express"); const path = require("path"); const bodyParser = require("body-parser"); const cors = require("cors");
Создание приложения Express
Давайте начнем с создания нашего приложения Express прямо сейчас. Для начала нам нужно получить возвращенный объект функции express()
назначенный новой переменной app
:
const app = express();
Обслуживание статических активов
Добавление строки выше после начального набора включений инициализирует наше app
как app
Express. Следующее, что нам нужно сделать, это настроить статические ресурсы. Давайте создадим новый каталог в нашем текущем проекте под названием public
и давайте использовать статическое промежуточное ПО Express для обслуживания статических файлов. Внутри каталога давайте создадим простой файл index.html
который говорит «Hello, World»:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>Hello, World</title> </head> <body> Hello, World! </body> </html>
Для обслуживания статических файлов у нас есть встроенная функция .use () с express.static () в Express. Синтаксис выглядит следующим образом:
app.use( express.static( path.join(__dirname, "public") ) );
Нам также нужно использовать промежуточное программное обеспечение анализатора тела для получения содержимого HTTP POST в виде JSON для доступа в req.body
. Мы также будем использовать urlencoded
для получения промежуточного программного обеспечения, которое анализирует только тела с urlencoded
и просматривает только те запросы, в которых заголовок Content-Type
соответствует параметру type
. Этот синтаксический анализатор принимает только кодировку тела UTF-8 и поддерживает автоматическое deflate
кодировок gzip
и deflate
:
app.use( bodyParser.json() ); app.use( bodyParser.urlencoded( { extended: false } ) );
Чтобы разрешить междоменные запросы, нам нужно включить CORS. Давайте включим модуль CORS, используя следующий код:
app.use( cors() );
Теперь вся начальная конфигурация установлена. Все, что нам нужно сделать сейчас, это установить порт и прослушивать входящие соединения на конкретном порту:
const port = 3000; app.listen(port, () => { console.log(`Server started on port ${port}.`); });
Убедитесь, что ваш последний app.js
выглядит так:
const express = require("express"); const path = require("path"); const bodyParser = require("body-parser"); const cors = require("cors"); // Create an App. const app = express(); // Serve the static files from public. app.use( express.static( path.join(__dirname, "public") ) ); // Include the body-parser middleware. app.use( bodyParser.json() ); app.use( bodyParser.urlencoded( { extended: false } ) ); // Enable CORS. app.use( cors() ); // Set the port. const port = 3000; // Listen to incoming connections. app.listen(port, () => { console.log(`Server started on port ${port}.`); });
Запустите команду для запуска сервера:
$ npm run dev
Откройте ваш http://localhost:3000/
на новой вкладке и увидите магию. Вы должны увидеть новую страницу с «Hello, World».
Создание бэк-энда приложения
Во-первых, давайте создадим каталог с именем vote.js
и создадим там файл, скажем, vote.js
. Нам нужно связать этот файл с нашим файлом app.js
, поэтому давайте вернемся к нему и включим его в нашу инициализацию express()
:
const app = express(); // Vote route. const vote = require("./routes/vote")
Поскольку каталог routes
находится в том же каталоге, что и app.js
, мы начнем с ./
. Чтобы использовать его с маршрутизатором, давайте перейдем к нижней части и добавим код промежуточного программного обеспечения маршрутизатора, показанный ниже, перед определением порта:
app.use("/vote", vote);
Все, что проходит через URL /vote
vote.js
, будет обрабатываться файлом vote.js
, предоставленным переменной vote
.
Обработка запросов GET и POST
Используя экземпляр Router Express, мы можем обрабатывать методы GET
и POST
через наш путь /vote
. Давайте сейчас создадим маршрут GET
умолчанию и отправим текст по умолчанию: "You are in /vote"
.
const express = require("express"); const router = express.Router(); // Default get route. router.get("/", (req, res) => { res.send("You are in /vote"); });
Приведенный выше код routes/vote.js
все запросы на путь /vote
в наши недавно сформированные routes/vote.js
.
Обработка POST-запросов
Нам также нужен обработчик POST
, где мы можем запустить Pusher API. Это будет Router.post()
для запросов POST
к /
так что все запросы будут Router.post()
/vote
из-за нашего промежуточного программного обеспечения. Здесь мы будем давать функции стрелок того же типа, и давайте дадим сообщение типа "You have POSTed to /vote."
:
// Default POST route. router.post("/", (req, res) => { res.send("You have POSTed to /vote."); });
Функция res.send()
будет заменена вызовами Pusher API в будущем.
Экспорт маршрутизатора
Наконец, мы должны экспортировать маршрутизатор как модуль. Используйте module.exports
как это в конце. Это должен быть конец файла, хотя вы можете иметь его где угодно. Помните, JavaScript ориентирован на события, а не процедурный:
// Export the router. module.exports = router;
На этом этапе, когда вы видите полный файл vote.js
, он должен выглядеть примерно так:
const express = require("express"); const router = express.Router(); // Default GET route. router.get("/", (req, res) => { res.send("You are in /vote."); }); // Default POST route. router.post("/", (req, res) => { res.send("You have POSTed to /vote."); }); // Export the router. module.exports = router;
Убедитесь, что вы сохранили все, и теперь попробуйте запустить оба URL в нашем веб-браузере.
Вы должны увидеть вывод в ваших веб-браузерах.
Интеграция с API Pusher
Давайте начнем с изменения кода, который мы написали для обработчика POST
— тот, который мы написали в файле vote.js
. Вот что мы на самом деле хотим вызвать Pusher. Давайте быстро перейдем к нашей панели инструментов Pusher, выберите ваше приложение Pusher (в моем случае — praveen-science-app
) и нажмите на вкладку « Начало работы ». Вы увидите код для запуска.
В нашем vote.js
мы должны определить (или потребовать) библиотеку Pusher. Затем нам нужно создать новый экземпляр (объект) класса Pusher
и, наконец, запустить службу Pusher
внутри POST
. Я vote.js
файл vote.js
как показано ниже:
Примечание: убедитесь, что вы изменили свой appId
, key
, secret
на тот, который указан на приборной панели.
const express = require("express"); const router = express.Router(); // ///// Step 1: Include Pusher ///// // const Pusher = require('pusher'); // ///// Step 2: Instantiate an Object ///// // const pusher = new Pusher({ appId: 'appId', key: 'key', secret: 'secret', cluster: 'eu', encrypted: true }); // Default GET route. router.get("/", (req, res) => { res.send("You are in /vote."); }); // Default POST route. router.post("/", (req, res) => { // ///// Step 3: Trigger the Pusher service ///// // pusher.trigger('my-channel', 'my-event', { "message": "hello world" }); }); // Export the router. module.exports = router;
В тот момент, когда пользователь отправляет форму, наше приложение запускает POST
запрос к этому маршруту, который собирается запустить Pusher API и запустить службу Pusher с помощью pusher.trigger()
функции pusher.trigger()
. Кроме того, мы не хотим использовать значения по умолчанию my-channel
и my-event
, поэтому давайте изменим их на hp-voting
и hp-house
. Нам также не нужно отправлять message
, но вместо этого мы хотели бы дать points
и информацию о house
:
router.post("/", (req, res) => { pusher.trigger('hp-voting', 'hp-house', { "points": 1, "house": req.body.house }); });
На данный момент мы собираемся присвоить points
значение 1
(я объясню почему в ближайшее время), и мы используем req.body.house
для house
, потому что значение будет поступать из данных формы и это обеспечивается с помощью req.body
как мы используем body-parser
.
Наконец, мы вернем JSON браузеру в качестве ответа, используя res.json()
и передадим объект с логическим success
и message
поблагодарившим пользователя за голосование и за то, что он был успешно получен:
router.post("/", (req, res) => { pusher.trigger('hp-voting', 'hp-house', { "points": 1, "house": req.body.house }); return res.json({ "success": true, "message": "Thanks for voting." }); });
Создание внешнего интерфейса приложения
Я использовал jQuery и Bootstrap для создания внешнего интерфейса. Вот часть, где мы разрешаем пользователям голосовать.
Я также собираюсь добавить контейнер графика, который будет отображать график в режиме реального времени по мере получения голосов.
Интегрируя все
У нас уже есть готовый конец. Теперь мы увидим, как отправить запрос в службу Pusher при нажатии кнопки голосования, благодаря внешнему JavaScript. Мы запустим событие submit
формы, когда пользователь нажмет кнопку, и он должен будет сделать запрос POST
к нашему внутреннему интерфейсу к маршруту /vote
.
Слушатель событий, пользовательские данные и Ajax
Давайте добавим прослушиватель событий для отправки формы, код для захвата пользовательских данных и вызов Ajax:
// Execute only after the whole document is fetched and assets are loaded. $(document).ready(function () { // Form submission event listener (event handler) $("#voteForm").submit(function (e) { e.preventDefault(); // Get the checked input element's value. var house = $(".form-check-input:checked").val(); // Construct the data to be sent as a payload to the AJAX call. var data = { "house": house }; $.post("/vote", data, function (res) { // Log the output in the console. console.log(res); }); }); });
Работа с толкателем и графиками
Когда форма отправляется, вызов Ajax запускает конечную точку routes/vote.js
, а внутреннее приложение Node также запускает службу Pusher, используя приведенный ниже код в routes/vote.js
:
pusher.trigger('hp-voting', 'hp-house', { "points": 1, "house": req.body.house });
Когда код выше нажал (или запустил), служба Pusher вызовет событие с hp-voting
и hp-house
. Мы еще не поймали событие и не подписались на него. Таким образом, мы собираемся реализовать CanvasJS для построения нашей диаграммы, и мы собираемся подписаться на вышеуказанное событие и будем добавлять точки данных с помощью триггера, указанного прослушивателем события submit
формы.
Добавление CanvasJS
Как только вы добавите все биты правильно, ваш script.js
стороне клиента должен быть похож на этот:
// Execute only after the whole document is fetched and assets are loaded. $(document).ready(function () { // Form submission event listener (event handler) $("#voteForm").submit(function (e) { // Prevent the default event. e.preventDefault(); // Get the checked input element's value. var house = $(".form-check-input:checked").val(); // Construct the data to be sent as a payload to the Ajax call. var data = { "house": house }; // Fire the POST request Ajax call to our /vote end point. $.post("/vote", data, function (res) { // Log the output in the console. console.log(res); }); }); // Create the base data points. var dataPoints = [ { label: "Gryffindor", y: 0 }, { label: "Hufflepuff", y: 0 }, { label: "Ravenclaw", y: 0 }, { label: "Slytherin", y: 0 } ]; // Initialize Chart using jQuery selector. // Get the chart container element. var chartContainer = $("#chartContainer"); // Check if the element exists in the DOM. if (chartContainer.length === 1) { // Construct the options for the chart. var options = { "animationEnabled": true, "theme": "light1", "title": { "text": "Harry Potter House Results" }, "data": [ { "type": "column", "dataPoints": dataPoints } ] }; // Initialize the chart. $("#chartContainer").CanvasJSChart(options); } });
Теперь сохраните файл, и когда вы перезагрузите страницу, вы сможете увидеть диаграмму заполнителя. Это, безусловно, реальный график, но без каких-либо значений. Вы должны увидеть что-то вроде этого:
Теперь мы реализовали нашу диаграмму CanvasJS с правой стороны.
Инициализация Pusher на стороне клиента
После регистрации Pusher мы должны инициализировать объект Pusher
. Поскольку у нас уже есть клиентский config.js
, мы будем использовать этот код в этой части:
// Initialise a Pusher Object. var pusher = new Pusher(PusherConfig.key, { cluster: PusherConfig.cluster, forceTLS: PusherConfigforceTLS. });
После инициализации объекта Pusher нам нужно подписаться на наш канал, где наши сообщения публикуются на стороне сервера. Мы будем копировать код с панели инструментов Pusher, но немного изменим, чтобы подписаться на наш канал hp-voting
hp-house
событие hp-house
. Значения по умолчанию my-channel
и my-event
должны быть обновлены следующим образом в соответствии с нашим внутренним кодом:
// Subscribe to the channel. var channel = pusher.subscribe('hp-voting'); // Bind to a particular event and listen to the event data. channel.bind('hp-house', function(data) { alert(JSON.stringify(data)); });
Вместо того, чтобы alert
о сообщении data
, мы хотим добавить данные в график. Способ, которым мы можем сделать это, беря наши dataPoints
и манипулируя массивом относительно ответа сервера. С уже существующей переменной dataPoints
(помните, что мы использовали var
вместо const
поскольку мы должны иметь возможность изменить ее на более позднем этапе), мы будем использовать функцию Array.map () более высокого порядка следующим образом:
// Bind to a particular event and listen to the event data. channel.bind('hp-house', function(data) { // Use a higher order Array map. dataPoints = dataPoints.map(function (d) { // Check if the current label is the updated value. if (d.label == data.house) { // Increment the house's value by the number of new points. dy += data.points; } // Return the original value as this is a map function. return d; }); });
Мы перебираем все метки dataPoints
, и когда конкретная метка соответствует текущей метке, мы будем увеличивать значение текущей метки с количеством обновленных точек. Поскольку мы используем функцию JavaScript Array.map()
, мы должны вернуть исходное значение d
обратно в вызов функции. После того, как мы обновили dataPoints
, мы должны повторно визуализировать диаграмму.
После функции map()
мы сделаем следующее:
channel.bind('hp-house', function(data) { // Use a higher order Array map. dataPoints = dataPoints.map(function (d) { // Check if the current label is the updated value. if (d.label == data.house) { // Increment the house's value by the number of new points. dy += data.points; } // Return the original value as this is a map function. return d; }); // Re-render the chart. $("#chartContainer").CanvasJSChart(options); });
Как только вы напишете весь приведенный выше код, сохраните и запустите приложение в своем браузере, запустите инструменты веб-разработчика и проверьте консоль. Вы должны увидеть, что Pusher Service связывается с вашим приложением. Я смог увидеть следующее в своей консоли (я скрыл свой appId
и secret
, поэтому кроме этих конфиденциальных битов информации все остальное показывается):
Вывод
В этот момент, когда вы пытаетесь открыть два окна одного и того же приложения и голосовать на одном экране, вы можете видеть, что все экраны обновляются одновременно. Это в основном то, как вы используете сервис Pusher для создания приложений в реальном времени.
Наше веб-приложение для голосования «Гарри Поттер» теперь отображает параметры (все четыре дома) и диаграмму справа, которая обновляется при голосовании подключенного пользователя. Следующим очевидным шагом будет использование базы данных, такой как MongoDB, для хранения всей информации, чтобы она сохранялась даже при перезагрузке страниц приложения.