Статьи

Создайте одностраничное приложение JavaScript без фреймворка

Фронтальные рамки великолепны. Они отвлекают большую часть сложности создания одностраничного приложения (SPA) и помогают вам разобраться в коде по мере развития вашего проекта.

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

Вот почему в этом уроке мы узнаем, как создать SPA с нуля, без использования клиентской среды JavaScript. Это поможет вам оценить, что эти фреймворки действительно делают для вас и в какой момент имеет смысл использовать один из них. Это также даст вам понимание частей, которые составляют типичный SPA и как они соединены вместе.

Давайте начнем …

Предпосылки

Для этого урока вам понадобятся фундаментальные знания современного JavaScript и jQuery . Некоторый опыт использования Handlebars , Express и Axios пригодится, хотя в этом нет особой необходимости. Вам также понадобится следующая настройка в вашей среде:

Вы можете найти завершенный проект в нашем репозитории GitHub .

Строим проект

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

  • отображать последние курсы валют
  • конвертировать из одной валюты в другую
  • отображать прошлые курсы валют на основе указанной даты.

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

Fixer — это хорошо продуманный API, который предоставляет JSON API для конвертации валют и конвертации валют. К сожалению, это коммерческая услуга, и бесплатный тариф не позволяет конвертировать валюту. Так что нам также нужно использовать API Free Currency Converter. API преобразования имеет несколько ограничений, которые, к счастью, не повлияют на функциональность нашего приложения. Доступ к нему можно получить напрямую, не требуя ключа API. Однако Fixer требуется ключ API для выполнения любого запроса. Просто зарегистрируйтесь на их сайте, чтобы получить ключ доступа к бесплатному плану .

В идеале, мы должны иметь возможность создать целое одностраничное приложение на стороне клиента. Однако, поскольку мы будем иметь дело с конфиденциальной информацией (наш ключ API), ее будет невозможно сохранить в нашем клиентском коде. Это сделает наше приложение уязвимым и открытым для любого младшего хакера, который сможет обойти приложение и получить доступ к данным непосредственно с наших конечных точек API. Чтобы защитить такую ​​конфиденциальную информацию, нам нужно поместить ее в код сервера. Итак, мы настроим сервер Express для работы в качестве прокси между клиентским кодом и облачными сервисами. Используя прокси, мы можем безопасно получить доступ к этому ключу, так как код сервера никогда не открывается браузеру. Ниже приведена схема, иллюстрирующая работу нашего завершенного проекта.

План проэкта

Обратите внимание на пакеты npm, которые будут использоваться каждой средой — то есть браузером (клиентом) и сервером. Теперь, когда вы знаете, что мы будем строить, перейдите к следующему разделу, чтобы начать создание проекта.

Каталоги и зависимости проекта

Перейдите в каталог вашей рабочей области и создайте папку single-page-application . Откройте папку в VSCode или в вашем любимом редакторе и создайте следующие файлы и папки с помощью терминала:

 touch .env .gitignore README.md server.js mkdir public lib mkdir public/js touch public/index.html touch public/js/app.js 

Откройте .gitignore и добавьте эти строки:

 node_modules .env 

Откройте README.md и добавьте эти строки:

 # Single Page Application This is a project demo that uses Vanilla JS to build a Single Page Application. 

Затем создайте файл package.json , выполнив следующую команду внутри терминала:

 npm init -y 

Вы должны получить следующий контент, сгенерированный для вас:

 { "name": "single-page-application", "version": "1.0.0", "description": "This is a project demo that uses Vanilla JS to build a Single Page Application.", "main": "server.js", "directories": { "lib": "lib" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js" }, "keywords": [], "author": "", "license": "ISC" } 

Видите, насколько удобна команда npm? Содержание было сгенерировано на основе структуры проекта. Давайте теперь установим основные зависимости, необходимые для нашего проекта. Выполните следующую команду в своем терминале:

 npm install jquery semantic-ui-css handlebars vanilla-router express dotenv axios 

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

База приложений

Прежде чем мы начнем писать наш код переднего плана, нам нужно реализовать базу сервер-клиент для работы. Это означает, что основное представление HTML обслуживается с сервера Express. Из соображений производительности и надежности мы добавим node_modules зависимости прямо из папки node_modules . Нам нужно будет настроить наш сервер Express специальным образом, чтобы это работало. Откройте server.js и добавьте следующее:

 require('dotenv').config(); // read .env files const express = require('express'); const app = express(); const port = process.env.PORT || 3000; // Set public folder as root app.use(express.static('public')); // Allow front-end access to node_modules folder app.use('/scripts', express.static(`${__dirname}/node_modules/`)); // Listen for HTTP requests on port 3000 app.listen(port, () => { console.log('listening on %d', port); }); 

Это дает нам базовый сервер Express. Я прокомментировал код, так что, надеюсь, это даст вам довольно хорошее представление о том, что происходит. Затем откройте public/index.html и введите:

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="scripts/semantic-ui-css/semantic.min.css"> <title>SPA Demo</title> </head> <body> <div class="ui container"> <!-- Navigation Menu --> <div class="ui four item inverted orange menu"> <div class="header item"> <i class="money bill alternate outline icon"></i> Single Page App </div> <a class="item" href="/"> Currency Rates </a> <a class="item" href="/exchange"> Exchange Rates </a> <a class="item" href="/historical"> Historical Rates </a> </div> <!-- Application Root --> <div id="app"></div> </div> <!-- JS Library Dependencies --> <script src="scripts/jquery/dist/jquery.min.js"></script> <script src="scripts/semantic-ui-css/semantic.min.js"></script> <script src="scripts/axios/dist/axios.min.js"></script> <script src="scripts/handlebars/dist/handlebars.min.js"></script> <script src="scripts/vanilla-router/dist/vanilla-router.min.js"></script> <script src="js/app.js"></script> </body> </html> 

Мы используем Semantic UI для стиля. Пожалуйста, обратитесь к документации Semantic UI Menu, чтобы понять код, используемый для нашей панели навигации. Зайдите в свой терминал и запустите сервер:

 npm start 

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

Панель навигации

Давайте теперь напишем несколько шаблонов представления для нашего приложения.

Скелетные шаблоны переднего плана

Мы будем использовать Handlebars для написания наших шаблонов. JavaScript будет использоваться для отображения шаблонов на основе текущего URL. Первый шаблон, который мы создадим, будет отображать сообщения об ошибках, таких как 404 или ошибки сервера. Поместите этот код в public/index.html сразу после раздела навигации:

 <!-- Error Template --> <script id="error-template" type="text/x-handlebars-template"> <div class="ui {{color}} inverted segment" style="height:250px;"> <br> <h2 class="ui center aligned icon header"> <i class="exclamation triangle icon"></i> <div class="content"> {{title}} <div class="sub header">{{message}}</div> </div> </h2> </div> </script> 

Затем добавьте следующие шаблоны, которые будут представлять представление для каждого URL-пути, указанного нами на панели навигации:

 <!-- Currency Rates Template --> <script id="rates-template" type="text/x-handlebars-template"> <h1 class="ui header">Currency Rates</h1> <hr> </script> <!-- Exchange Conversion Template --> <script id="exchange-template" type="text/x-handlebars-template"> <h1 class="ui header">Exchange Conversion</h1> <hr> </script> <!-- Historical Rates Template --> <script id="historical-template" type="text/x-handlebars-template"> <h1 class="ui header">Historical Rates</h1> <hr> </script> 

Далее, давайте скомпилируем все шаблоны тезисов в public/js/app.js После компиляции мы отрендерим rates-template и посмотрим, как он выглядит:

 window.addEventListener('load', () => { const el = $('#app'); // Compile Handlebar Templates const errorTemplate = Handlebars.compile($('#error-template').html()); const ratesTemplate = Handlebars.compile($('#rates-template').html()); const exchangeTemplate = Handlebars.compile($('#exchange-template').html()); const historicalTemplate = Handlebars.compile($('#historical-template').html()); const html = ratesTemplate(); el.html(html); }); 

Обратите внимание, что мы оборачиваем весь клиентский код JavaScript внутри события load . Это просто для того, чтобы убедиться, что все зависимости были загружены и DOM завершил загрузку. Обновите страницу и посмотрите, что у нас есть:

Курсы валют пустые

Мы делаем успехи. Теперь, если вы нажмете на другие ссылки, кроме Курсов валют , браузер попытается получить новую страницу и в итоге получит сообщение, подобное этому: Cannot GET /exchange .

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

Маршрутизация на стороне клиента

Чтобы управлять маршрутизацией в среде браузера, нам нужно реализовать маршрутизацию на стороне клиента. Есть много клиентских библиотек маршрутизации, которые могут помочь с этим. Для нашего проекта мы будем использовать vanilla router , который представляет собой очень простой в использовании пакет маршрутизации.

Если вы помните, мы ранее включили все библиотеки JavaScript, которые нам нужны, в index.html . Следовательно, мы можем сразу вызвать класс Router . Удалите последние два оператора, которые вы добавили в app.js и замените их следующим кодом:

 // Router Declaration const router = new Router({ mode: 'history', page404: (path) => { const html = errorTemplate({ color: 'yellow', title: 'Error 404 - Page NOT Found!', message: `The path '/${path}' does not exist on this site`, }); el.html(html); }, }); router.add('/', () => { let html = ratesTemplate(); el.html(html); }); router.add('/exchange', () => { let html = exchangeTemplate(); el.html(html); }); router.add('/historical', () => { let html = historicalTemplate(); el.html(html); }); // Navigate app to current url router.navigateTo(window.location.pathname); // Highlight Active Menu on Refresh/Page Reload const link = $(`a[href$='${window.location.pathname}']`); link.addClass('active'); $('a').on('click', (event) => { // Block browser page load event.preventDefault(); // Highlight Active Menu on Click const target = $(event.target); $('.item').removeClass('active'); target.addClass('active'); // Navigate to clicked url const href = target.attr('href'); const path = href.substr(href.lastIndexOf('/')); router.navigateTo(path); }); 

Потратьте некоторое время на просмотр кода. Я добавил комментарии в различных разделах, чтобы объяснить, что происходит. Вы заметите, что в объявлении маршрутизатора мы указали свойство page404 для использования шаблона ошибки. Давайте теперь проверим ссылки:

Исторический бланк навигации

Ссылки теперь должны работать. Но у нас есть проблема. Нажмите ссылку /exchange или historical ссылку, затем обновите браузер. Мы получаем ту же ошибку, что и раньше — Cannot GET /exchange . Чтобы это исправить, server.js к server.js и добавьте этот оператор прямо перед кодом прослушивания:

 // Redirect all traffic to index.html app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`)); 

Вам придется перезапустить сервер с помощью Ctrl + C и выполнить npm start . Вернитесь в браузер и попробуйте обновить. Теперь вы должны правильно увидеть страницу рендера. Теперь давайте попробуем ввести несуществующий путь в URL, например /exchanges . Приложение должно отображать сообщение об ошибке 404:

Ошибка 404

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

Последние курсы валют

Для этой задачи мы будем использовать конечную точку Fixer Latest Rates . Откройте файл .env и добавьте свой ключ API. Мы также укажем время ожидания и символы, которые мы перечислим на нашей странице. Не стесняйтесь увеличивать значение тайм-аута, если у вас медленное интернет-соединение:

 API_KEY=<paste key here> PORT=3000 TIMEOUT=5000 SYMBOLS=EUR,USD,GBP,AUD,BTC,KES,JPY,CNY 

Затем создайте файл lib/fixer-service.js . Здесь мы напишем вспомогательный код для нашего сервера Express, чтобы легко запрашивать информацию у Fixer. Скопируйте следующий код:

 require('dotenv').config(); const axios = require('axios'); const symbols = process.env.SYMBOLS || 'EUR,USD,GBP'; // Axios Client declaration const api = axios.create({ baseURL: 'http://data.fixer.io/api', params: { access_key: process.env.API_KEY, }, timeout: process.env.TIMEOUT || 5000, }); // Generic GET request function const get = async (url) => { const response = await api.get(url); const { data } = response; if (data.success) { return data; } throw new Error(data.error.type); }; module.exports = { getRates: () => get(`/latest&symbols=${symbols}&base=EUR`), }; 

Опять же, потратьте некоторое время на просмотр кода, чтобы понять, что происходит. Если вы не уверены, вы также можете проверить документацию для dotenv , axios и ознакомиться с экспортом модулей . Теперь давайте сделаем быстрый тест, чтобы убедиться, что getRates() работает.

Откройте server.js и добавьте этот код:

 const { getRates } = require('./lib/fixer-service'); ... // Place this block at the bottom const test = async() => { const data = await getRates(); console.log(data); } test(); 

Запустите npm start или node server . Через несколько секунд вы должны получить следующий вывод:

 { success: true, timestamp: 1523871848, base: 'EUR', date: '2018-04-16', rates: { EUR: 1, USD: 1.23732, GBP: 0.865158, AUD: 1.59169, BTC: 0.000153, KES: 124.226892, JPY: 132.608498, CNY: 7.775567 } } 

Если вы получаете что-то похожее на вышеприведенное, это означает, что код работает. Значения, конечно, будут разными, так как цены меняются каждый день. Теперь закомментируйте тестовый блок и вставьте этот код прямо перед оператором, который перенаправляет весь трафик в index.html :

 // Express Error handler const errorHandler = (err, req, res) => { if (err.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx res.status(403).send({ title: 'Server responded with an error', message: err.message }); } else if (err.request) { // The request was made but no response was received res.status(503).send({ title: 'Unable to communicate with server', message: err.message }); } else { // Something happened in setting up the request that triggered an Error res.status(500).send({ title: 'An unexpected error occurred', message: err.message }); } }; // Fetch Latest Currency Rates app.get('/api/rates', async (req, res) => { try { const data = await getRates(); res.setHeader('Content-Type', 'application/json'); res.send(data); } catch (error) { errorHandler(error, req, res); } }); 

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

Давайте подтвердим, что этот бит кода работает. Перезапустите сервер Express и перейдите в браузере по этому адресу: localhost: 3000 / api / rate . Вы должны увидеть тот же результат JSON, который был показан в консоли. Теперь мы можем реализовать представление, которое будет отображать эту информацию в аккуратной элегантной таблице.

Откройте public/index.html и замените rates-template следующим кодом:

 <!-- Currency Rates Template --> <script id="rates-template" type="text/x-handlebars-template"> <h1 class="ui header">Currency Rates</h1> <hr> <div class="ui loading basic segment"> <div class="ui horizontal list"> <div class="item"> <i class="calendar alternate outline icon"></i> <div class="content"> <div class="ui sub header">Date</div> <span>{{date}}</span> </div> </div> <div class="item"> <i class="money bill alternate outline icon"></i> <div class="content"> <div class="ui sub header">Base</div> <span>{{base}}</span> </div> </div> </div> <table class="ui celled striped selectable inverted table"> <thead> <tr> <th>Code</th> <th>Rate</th> </tr> </thead> <tbody> {{#each rates}} <tr> <td>{{@key}}</td> <td>{{this}}</td> </tr> {{/each}} </tbody> </table> </div> </script> 

Помните, что мы используем Semantic UI, чтобы предоставить нам стиль. Я бы хотел, чтобы вы обратили пристальное внимание на компонент загрузки сегмента . Это будет указанием, чтобы пользователи знали, что что-то происходит, когда приложение извлекает данные. Мы также используем табличный интерфейс для отображения ставок. Пожалуйста, перейдите по соответствующей документации, если вы новичок в Semantic.

Теперь давайте обновим наш код в public/js/app.js чтобы использовать этот новый шаблон. Замените первую route.add('/') следующим кодом:

 // Instantiate api handler const api = axios.create({ baseURL: 'http://localhost:3000/api', timeout: 5000, }); // Display Error Banner const showError = (error) => { const { title, message } = error.response.data; const html = errorTemplate({ color: 'red', title, message }); el.html(html); }; // Display Latest Currency Rates router.add('/', async () => { // Display loader first let html = ratesTemplate(); el.html(html); try { // Load Currency Rates const response = await api.get('/rates'); const { base, date, rates } = response.data; // Display Rates Table html = ratesTemplate({ base, date, rates }); el.html(html); } catch (error) { showError(error); } finally { // Remove loader status $('.loading').removeClass('loading'); } }); 

Первый блок кода создает клиент API для связи с нашим прокси-сервером. Второй блок — это глобальная функция для обработки ошибок. Его работа заключается просто в отображении баннера с ошибкой на случай, если что-то пойдет не так на стороне сервера. В третьем блоке мы получаем данные о тарифах от localhost:3000/api/rates конечную точку localhost:3000/api/rates и передаем их в rates-template для отображения информации.

Просто обновите браузер. Теперь у вас должно появиться следующее представление:

Курсы валют

Далее мы создадим интерфейс для конвертации валют.

Обмен Конверсия

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

Нам нужна конечная точка символов, чтобы получить список поддерживаемых кодов валют. Мы будем использовать эти данные, чтобы заполнить выпадающие списки, которые пользователи будут использовать для выбора валют для конвертации. Откройте lib/fixer-service.js и добавьте эту строку сразу после функции getRates() :

 getSymbols: () => get('/symbols'), 

Создайте еще один вспомогательный файл lib/free-currency-service.js и добавьте следующий код:

 require('dotenv').config(); const axios = require('axios'); const api = axios.create({ baseURL: 'https://free.currencyconverterapi.com/api/v5', timeout: process.env.TIMEOUT || 5000, }); module.exports = { convertCurrency: async (from, to) => { const response = await api.get(`/convert?q=${from}_${to}&compact=y`); const key = Object.keys(response.data)[0]; const { val } = response.data[key]; return { rate: val }; }, }; 

Это поможет нам получить бесплатный обменный курс из одной валюты в другую. В коде клиента мы должны рассчитать сумму конвертации путем умножения суммы на ставку. Теперь давайте добавим эти два метода обслуживания в наш серверный код Express. Откройте server.js и обновите соответственно:

 const { getRates, getSymbols, } = require('./lib/fixer-service'); const { convertCurrency } = require('./lib/free-currency-service'); ... // Insert right after get '/api/rates', just before the redirect statement // Fetch Symbols app.get('/api/symbols', async (req, res) => { try { const data = await getSymbols(); res.setHeader('Content-Type', 'application/json'); res.send(data); } catch (error) { errorHandler(error, req, res); } }); // Convert Currency app.post('/api/convert', async (req, res) => { try { const { from, to } = req.body; const data = await convertCurrency(from, to); res.setHeader('Content-Type', 'application/json'); res.send(data); } catch (error) { errorHandler(error, req, res); } }); 

Теперь наш прокси-сервер должен иметь возможность получать символы и показатели конверсии. Обратите внимание, что /api/convert является методом POST. Мы будем использовать форму на стороне клиента для создания интерфейса конвертации валют. Не стесняйтесь использовать test функцию, чтобы убедиться, что обе конечные точки работают. Вот пример:

 // Test Symbols Endpoint const test = async() => { const data = await getSymbols(); console.log(data); } // Test Currency Conversion Endpoint const test = async() => { const data = await convertCurrency('USD', 'KES'); console.log(data); } 

Вам нужно будет перезапустить сервер для каждого теста. Не забудьте закомментировать тесты, как только вы подтвердите, что код работает. Давайте теперь поработаем над нашим интерфейсом конвертации валют. Откройте public/index.html и обновите exchange-template , заменив существующий код следующим:

 <script id="exchange-template" type="text/x-handlebars-template"> <h1 class="ui header">Exchange Rate</h1> <hr> <div class="ui basic loading segment"> <form class="ui form"> <div class="three fields"> <div class="field"> <label>From</label> <select class="ui dropdown" name="from" id="from"> <option value="">Select Currency</option> {{#each symbols}} <option value="{{@key}}">{{this}}</option> {{/each}} </select> </div> <div class="field"> <label>To</label> <select class="ui dropdown" name="to" id="to"> <option value="">Select Currency</option> {{#each symbols}} <option value="{{@key}}">{{this}}</option> {{/each}} </select> </div> <div class="field"> <label>Amount</label> <input type="number" name="amount" id="amount" placeholder="Enter amount"> </div> </div> <div class="ui primary submit button">Convert</div> <div class="ui error message"></div> </form> <br> <div id="result-segment" class="ui center aligned segment"> <h2 id="result" class="ui header"> 0.00 </h2> </div> </div> </script> 

Не торопитесь, чтобы пройти сценарий и понять, что происходит. Мы используем Semantic UI Form для создания интерфейса. Мы также используем нотацию Handlebars для заполнения выпадающих списков. Ниже приведен формат JSON, используемый конечной точкой символов Fixer:

 { "success": true, "symbols": { "AED": "United Arab Emirates Dirham", "AFN": "Afghan Afghani", "ALL": "Albanian Lek", "AMD": "Armenian Dram", } } 

Обратите внимание, что данные символов представлены в формате карты. Это означает, что информация хранится в виде {{@key}} ключей {{@key}} и значений {{this}} . Давайте теперь обновим public/js/app.js и заставим его работать с новым шаблоном. Откройте файл и замените существующий код маршрута для /exchange следующим:

 // Perform POST request, calculate and display conversion results const getConversionResults = async () => { // Extract form data const from = $('#from').val(); const to = $('#to').val(); const amount = $('#amount').val(); // Send post data to Express(proxy) server try { const response = await api.post('/convert', { from, to }); const { rate } = response.data; const result = rate * amount; $('#result').html(`${to} ${result}`); } catch (error) { showError(error); } finally { $('#result-segment').removeClass('loading'); } }; // Handle Convert Button Click Event const convertRatesHandler = () => { if ($('.ui.form').form('is valid')) { // hide error message $('.ui.error.message').hide(); // Post to Express server $('#result-segment').addClass('loading'); getConversionResults(); // Prevent page from submitting to server return false; } return true; }; router.add('/exchange', async () => { // Display loader first let html = exchangeTemplate(); el.html(html); try { // Load Symbols const response = await api.get('/symbols'); const { symbols } = response.data; html = exchangeTemplate({ symbols }); el.html(html); $('.loading').removeClass('loading'); // Validate Form Inputs $('.ui.form').form({ fields: { from: 'empty', to: 'empty', amount: 'decimal', }, }); // Specify Submit Handler $('.submit').click(convertRatesHandler); } catch (error) { showError(error); } }); 

Обновите страницу. Теперь у вас должно появиться следующее представление:

Интерфейс обмена

Выберите несколько валют по вашему выбору и введите сумму. Затем нажмите кнопку « Преобразовать» :

Ошибка обмена

К сожалению! Мы просто столкнулись с ошибкой. По крайней мере, мы знаем, что наш код обработки ошибок работает. Чтобы выяснить причину возникновения ошибки, вернитесь к коду сервера и посмотрите на функцию /api/convert . В частности, посмотрите на строку, которая говорит const { from, to } = req.body; ,

Кажется, Express не может прочитать свойства из объекта request . Чтобы это исправить, нам нужно установить промежуточное программное обеспечение, которое может помочь с этим:

 npm install body-parser 

Затем обновите код сервера следующим образом:

 const bodyParser = require('body-parser'); ... /** Place this code right before the error handler function **/ // Parse POST data as URL encoded data app.use(bodyParser.urlencoded({ extended: true, })); // Parse POST data as JSON app.use(bodyParser.json()); 

Запустите сервер снова и обновите браузер. Попробуйте сделать другое преобразование. Теперь должно работать.

Обмен конвертации

Давайте теперь сосредоточимся на последнем бите — исторических курсах валют. Давайте начнем с представлений.

Исторические курсы валют

Реализация этой функции будет похожа на объединение задач с первой и второй страниц. Мы собираемся создать крошечную форму, в которой пользователь должен будет ввести дату. Когда пользователь нажимает кнопку «Отправить», курсы валют на указанную дату будут отображаться в табличном формате. Для этого мы будем использовать конечную точку исторических курсов из Fixer API. Запрос API выглядит следующим образом:

 https://data.fixer.io/api/2013-12-24 ? access_key = API_KEY & base = GBP & symbols = USD,CAD,EUR 

И ответ будет выглядеть так:

 { "success": true, "historical": true, "date": "2013-12-24", "timestamp": 1387929599, "base": "GBP", "rates": { "USD": 1.636492, "EUR": 1.196476, "CAD": 1.739516 } } 

Откройте lib/fixer-service.js и lib/fixer-service.js Historical Rates следующим образом:

 ... /** Place right after getSymbols **/ getHistoricalRate: date => get(`/${date}&symbols=${symbols}&base=EUR`), ... 

Откройте server.js и добавьте этот код:

 ... const { getRates, getSymbols, getHistoricalRate } = require('./lib/fixer-service'); ... /** Place this after '/api/convert' post function **/ // Fetch Currency Rates by date app.post('/api/historical', async (req, res) => { try { const { date } = req.body; const data = await getHistoricalRate(date); res.setHeader('Content-Type', 'application/json'); res.send(data); } catch (error) { errorHandler(error, req, res); } }); ... 

Если вы сомневаетесь в том, как устроен код, обратитесь к полному файлу server.js на GitHub . Не стесняйтесь написать быстрый тест, чтобы подтвердить, что историческая конечная точка работает:

 const test = async() => { const data = await getHistoricalRate('2012-07-14'); console.log(data); } test(); 

Не забудьте закомментировать тестовый блок, как только вы подтвердите, что все работает. Теперь давайте теперь поработаем над клиентским кодом.

Откройте index.html . Удалите существующий historical-template мы использовали в качестве заполнителя, и замените его следующим:

 <script id="historical-template" type="text/x-handlebars-template"> <h1 class="ui header">Historical Rates</h1> <hr> <form class="ui form"> <div class="field"> <label>Pick Date</label> <div class="ui calendar" id="calendar"> <div class="ui input left icon"> <i class="calendar icon"></i> <input type="text" placeholder="Date" id="date"> </div> </div> </div> <div class="ui primary submit button">Fetch Rates</div> <div class="ui error message"></div> </form> <div class="ui basic segment"> <div id="historical-table"></div> </div> </script> 

Сначала посмотрите на форму. Одна вещь, на которую я хотел бы обратить внимание, это то, что в Semantic UI официально нет ввода даты. Однако благодаря вкладу Майкла де Хуга у нас есть модуль Semantic-UI-Calendar . Просто установите его, используя npm:

 npm install semantic-ui-calendar 

Вернитесь к public/index.html и public/index.html его в раздел скриптов:

 ... <script src="scripts/semantic-ui-css/semantic.min.js"></script> <script src="scripts/semantic-ui-calendar/dist/calendar.min.js"></script> .... 

Чтобы отобразить исторические тарифы, мы просто повторно используем rates-template . Затем откройте public/js/app.js и обновите существующий код маршрута для /historical public/js/app.js :

 const getHistoricalRates = async () => { const date = $('#date').val(); try { const response = await api.post('/historical', { date }); const { base, rates } = response.data; const html = ratesTemplate({ base, date, rates }); $('#historical-table').html(html); } catch (error) { showError(error); } finally { $('.segment').removeClass('loading'); } }; const historicalRatesHandler = () => { if ($('.ui.form').form('is valid')) { // hide error message $('.ui.error.message').hide(); // Indicate loading status $('.segment').addClass('loading'); getHistoricalRates(); // Prevent page from submitting to server return false; } return true; }; router.add('/historical', () => { // Display form const html = historicalTemplate(); el.html(html); // Activate Date Picker $('#calendar').calendar({ type: 'date', formatter: { //format date to yyyy-mm-dd date: date => new Date(date).toISOString().split('T')[0], }, }); // Validate Date input $('.ui.form').form({ fields: { date: 'empty', }, }); $('.submit').click(historicalRatesHandler); }); 

Еще раз, найдите время, чтобы прочитать комментарии и понять код и то, что он делает. Затем перезагрузите сервер, обновите браузер и перейдите к /historical пути. Выберите любую дату до 1999 года, затем нажмите «Выбрать цены» . У вас должно быть что-то вроде этого:

Исторические курсы Тодо

Если вы выберете дату до 1999 года или дату в будущем, при отправке формы будет отображаться баннер с ошибкой.

Историческая ошибка Тодо

Резюме

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

  • DOM Performance . В нашем клиентском коде мы напрямую манипулируем DOM. Это может скоро выйти из-под контроля по мере роста проекта, в результате чего пользовательский интерфейс станет вялым.

  • Производительность браузера . Существует довольно много интерфейсных библиотек, которые мы загрузили в качестве скриптов в index.html , что хорошо для целей разработки. Для производственного развертывания нам нужна система для объединения всех сценариев, чтобы браузеры использовали один запрос для загрузки необходимых ресурсов JavaScript.

  • Монолитный кодекс Для серверного кода проще разбить код на модульные части, поскольку он работает в среде Node. Тем не менее, для клиентского кода нелегко организовать его в модули, если вы не используете такой пакет, как webpack .

  • Тестирование До сих пор мы проводили ручное тестирование. Для готового к работе приложения нам нужно настроить среду тестирования, такую ​​как Jasmine, Mocha или Chai, чтобы автоматизировать эту работу. Это поможет предотвратить повторяющиеся ошибки.

Это лишь некоторые из многих проблем, с которыми вы столкнетесь, когда будете подходить к разработке проекта без использования фреймворка. Использование чего-либо, такого как Angular, React или Vue, поможет вам решить многие из этих проблем. Я надеюсь, что это руководство было полезным и поможет вам стать профессиональным разработчиком JavaScript.