Делаем современное веб-приложение с нуля
Итак, вы решили сделать новый проект. И проект этот — веб-приложение. Сколько времени уйдёт на создание базового прототипа? Насколько это сложно? Что должен уже со старта уметь современный веб-сайт?
В этой статье мы попробуем набросать boilerplate простейшего веб-приложения со следующей архитектурой:
Введение
Перед разработкой, конечно, сперва нужно определиться, что мы разрабатываем! В качестве модельного приложения для этой статьи я решил сделать примитивный wiki-движок. У нас будут карточки, оформленные в Markdown; их можно будет смотреть и (когда-нибудь в будущем) предлагать правки. Всё это мы оформим в виде одностраничного приложения с server-side rendering (что совершенно необходимо для индексации наших будущих терабайт контента).
Давайте чуть подробнее пройдёмся по компонентам, которые нам для этого понадобятся:
Инфраструктура: git
Наверное, про это можно было и не говорить, но, конечно, мы будем вести разработку в git-репозитории.
Итоговый проект можно посмотреть на Github. Каждой секции статьи соответствует один коммит (я немало ребейзил, чтобы добиться этого!).
Инфраструктура: docker-compose
Начнём с настройки окружения. При том изобилии компонент, которое у нас имеется, весьма логичным решением для разработки будет использование docker-compose.
Добавим в репозиторий файл docker-compose.yml следующего содержания:
Давайте разберём вкратце, что тут происходит.
Не менее важный docker/backend/.dockerignore :
Воркер в целом аналогичен бэкенду, только вместо gunicorn у нас обычный запуск питонячьего модуля:
Наконец, фронтенд. Про него на Хабре есть целая отдельная статья, но, судя по развернутой дискуссии на StackOverflow и комментариям в духе «Ребят, уже 2018, нормального решения всё ещё нет?» там всё не так просто. Я остановился на таком варианте докерфайла.
Итак, наш каркас из контейнеров готов и можно наполнять его содержимым!
Бэкенд: каркас на Flask
Теперь наш бэкенд мы можем официально ПОДНЯТЬ!
Фронтенд: каркас на Express
В дальнейшем нам вообще не потребуется Node.js на машине разработчика (хотя мы могли и сейчас извернуться и запустить npm init через Docker, ну да ладно).
В Dockerfile мы упомянули npm run build и npm run start — нужно добавить в package.json соответствующие команды:
Команда build пока ничего не делает, но она нам ещё пригодится.
Добавим в зависимости Express и создадим в index.js простое приложение:
Теперь docker-compose up frontend поднимает наш фронтенд! Более того, на http://localhost:40002 уже должно красоваться классическое “Hello, world”.
Фронтенд: сборка с webpack и React-приложение
Пришло время изобразить в нашем приложении нечто больше, чем plain text. В этой секции мы добавим простейший React-компонент App и настроим сборку.
При программировании на React очень удобно использовать JSX — диалект JavaScript, расширенный синтаксическими конструкциями вида
Однако, JavaScript-движки не понимают его, поэтому обычно во фронтенд добавляется этап сборки. Специальные компиляторы JavaScript (ага-ага) превращают синтаксический сахар в уродливый классический JavaScript, обрабатывают импорты, минифицируют и так далее.
2014 год. apt-cache search java
Итак, простейший React-компонент выглядит очень просто.
Он просто выведет на экран наше приветствие более убедительным кеглем.
Добавим и клиентскую точку входа:
Для сборки всей этой красоты нам потребуются:
webpack — модный молодёжный сборщик для JS (хотя я уже три часа не читал статей по фронтенду, так что насчёт моды не уверен);
babel — компилятор для всевозможных примочек вроде JSX, а заодно поставщик полифиллов на все случаи IE.
Если предыдущая итерация фронтенда у вас всё ещё запущена, вам достаточно сделать
для установки новых зависимостей. Теперь настроим webpack:
Чтобы заработал babel, нужно сконфигурировать frontend/.babelrc :
Наконец, сделаем осмысленной нашу команду npm run build :
Бэкенд: данные в MongoDB
Прежде, чем двигаться дальше и вдыхать в наше приложение жизнь, надо сперва её вдохнуть в бэкенд. Кажется, мы собирались хранить размеченные в Markdown карточки — пора это сделать.
В то время, как существуют ORM для MongoDB на питоне, я считаю использование ORM практикой порочной и оставляю изучение соответствующих решений на ваше усмотрение. Вместо этого сделаем простенький класс для карточки и сопутствующий DAO:
(Если вы до сих пор не используете аннотации типов в Python, обязательно гляньте эти статьи!)
Теперь надо создать MongoCardDAO и дать Flask-приложению к нему доступ. Хотя сейчас у нас очень простая иерархия объектов (настройки → клиент pymongo → база данных pymongo → MongoCardDAO ), давайте сразу создадим централизованный царь-компонент, делающий dependency injection (он пригодится нам снова, когда мы будем делать воркер и tools).
Время добавить новый роут в Flask-приложение и наслаждаться видом!
Упс… ох, точно. Нам же нужно добавить контент! Заведём папку tools и сложим в неё скриптик, добавляющий одну тестовую карточку:
Успех! Теперь время поддержать это на фронтенде.
Фронтенд: Redux
Начнём с добавления Redux. Redux — JavaScript-библиотека для хранения состояния. Идея в том, чтобы вместо тысячи неявных состояний, изменяемых вашими компонентами при пользовательских действиях и других интересных событиях, иметь одно централизованное состояние, а любое изменение его производить через централизованный механизм действий. Так, если раньше для навигации мы сперва включали гифку загрузки, потом делали запрос через AJAX и, наконец, в success-коллбеке прописывали обновление нужных частей страницы, то в Redux-парадигме нам предлагается отправить действие “изменить контент на гифку с анимацией”, которое изменит глобальное состояние так, что одна из ваших компонент выкинет прежний контент и поставит анимацию, потом сделать запрос, а в его success-коллбеке отправить ещё одно действие, “изменить контент на подгруженный”. В общем, сейчас мы это сами увидим.
Начнём с установки новых зависимостей в наш контейнер.
Первое — собственно, Redux, второе — специальная библиотека для скрещивания React и Redux (written by mating experts), третье — очень нужная штука, необходимость который неплохо обоснована в её же README, и, наконец, четвёртое — библиотечка, необходимая для работы Redux DevTools Extension.
Начнём с бойлерплейтного Redux-кода: создания редьюсера, который ничего не делает, и инициализации состояния.
Наш клиент немного видоизменяется, морально готовясь к работе с Redux:
Фронтенд: страница карточки
Прежде, чем сделать страницы с SSR, надо сделать страницы без SSR! Давайте наконец воспользуемся нашим гениальным API для доступа к карточкам и сверстаем страницу карточки на фронтенде.
Время воспользоваться интеллектом и задизайнить структуру нашего состояния. Материалов на эту тему довольно много, так что предлагаю интеллектом не злоупотреблять и остановится на простом. Например, таком:
Заведём компонент «карточка», принимающий в качестве props содержимое cardData (оно же — фактически содержимое нашей карточки в mongo):
Теперь заведём компонент для всей страницы с карточкой. Он будет ответственен за то, чтобы достать нужные данные из API и передать их в Card. А фетчинг данных мы сделаем React-Redux way.
Для начала создадим файлик frontend/src/redux/actions.js и создадим действие, которые достаёт из API содержимое карточки, если ещё не:
Ох, у нас появилось действие, которое ЧТО-ТО ДЕЛАЕТ! Это надо поддержать в редьюсере:
(Обратите внимание на сверхмодный синтаксис для клонирования объекта с изменением отдельных полей.)
Теперь, когда вся логика унесена в Redux actions, сама компонента CardPage будет выглядеть сравнительно просто:
Добавим простенькую обработку page.type в наш корневой компонент App:
И теперь остался последний момент — надо как-то инициализировать page.type и page.cardSlug в зависимости от URL страницы.
Но в этой статье ещё много разделов, мы же не можем сделать качественное решение прямо сейчас. Давайте пока что сделаем это как-нибудь глупо. Вот прям совсем глупо. Например, регуляркой при инициализации приложения!
Так, секундочку… а где же наш контент? Ох, да мы ведь забыли распарсить Markdown!
Воркер: RQ
Парсинг Markdown и генерация HTML для карточки потенциально неограниченного размера — типичная «тяжёлая» задача, которую вместо того, чтобы решать прямо на бэкенде при сохранении изменений, обычно ставят в очередь и исполняют на отдельных машинах — воркерах.
Есть много опенсорсных реализаций очередей задач; мы возьмём Redis и простенькую библиотечку RQ (Redis Queue), которая передаёт параметры задач в формате pickle и сама организует нам спаунинг процессов для их обработки.
Время добавить редис в зависимости, настройки и вайринг!
Немного бойлерплейтного кода для воркера.
Для самого парсинга подключим библиотечку mistune и напишем простенькую функцию:
Мы объявили свой класс джобы, прокидывающий вайринг в качестве дополнительного kwargs-аргумента во все таски. (Обратите внимание, что он создаёт каждый раз НОВЫЙ вайринг, потому что некоторые клиенты нельзя создавать перед форком, который происходит внутри RQ перед началом обработки задачи.) Чтобы все наши таски не стали зависеть от вайринга — то есть от ВСЕХ наших объектов, — давайте сделаем декоратор, который будет доставать из вайринга только нужное:
Добавляем декоратор к нашей таске и радуемся жизни:
Радуемся жизни? Тьфу, я хотел сказать, запускаем воркер:
Ииии… он ничего не делает! Конечно, ведь мы не ставили ни одной таски!
Давайте перепишем нашу тулзу, которая создаёт тестовую карточку, чтобы она: а) не падала, если карточка уже создана (как в нашем случае); б) ставила таску на парсинг маркдауна.
Пересобрав контейнер с бэкендом, мы наконец можем увидеть контент нашей карточки в браузере:
Фронтенд: навигация
Прежде, чем мы перейдём к SSR, нам нужно сделать всю нашу возню с React хоть сколько-то осмысленной и сделать наше single page application действительно single page. Давайте обновим нашу тулзу, чтобы создавалось две (НЕ ОДНА, А ДВЕ! МАМА, Я ТЕПЕРЬ БИГ ДАТА ДЕВЕЛОПЕР!) карточки, ссылающиеся друг на друга, и потом займёмся навигацией между ними.
Теперь мы можем ходить по ссылкам и созерцать, как каждый раз наше чудесное приложение перезагружается. Хватит это терпеть!
Сперва навесим свой обработчик на клики по ссылкам. Поскольку HTML со ссылками у нас приходит с бэкенда, а приложение у нас на React, потребуется небольшой React-специфический фокус.
Добавляем глупенький редьюсер под это дело:
Внимательный читатель обратит внимание, что URL страницы не будет изменяться при навигации между карточками — даже на скриншоте мы видим Hello, world-карточку по адресу demo-карточки. Соответственно, навигация вперёд-назад тоже отвалилась. Давайте сразу добавим немного чёрной магии с history, чтобы починить это!
Теперь при переходах по ссылкам URL в адресной строке браузера будет реально меняться. Однако, кнопка «Назад» сломается!
А вот как — действие navigate:
Вот теперь история заработает.
Фронтенд: server-side rendering
Пришло время для нашей главной (на мой взгляд) фишечки — SEO-дружелюбия. Чтобы поисковики могли индексировать наш контент, полностью создаваемый динамически в React-компонентах, нам нужно уметь выдавать им результат рендеринга React, и ещё и научиться потом делать этот результат снова интерактивным.
Как создать свое первое веб-приложение с помощью Go
Привет, Хабр! Представляю вашему вниманию перевод статьи «How to build your first web application with Go» автора Ayooluwa Isaiah.
Это руководство к вашему первому веб-приложению на Go. Мы создадим новостное приложение, которое использует News API для получения новостных статей по определенной теме, и развернём его на продакшн сервере в конце.
Вы можете найти полный код, используемый для этого урока в этом GitHub репозитории.
Требования
Если вы считаете это задание слишком сложным для вас, перейдите к моему предыдущему вводному уроку по языку, который должен помочь вам освоиться.
Итак, начнем!
Создадим базовый веб-сервер
Давайте начнем с создания базового сервера, который отправляет текст «Hello World!» в браузер при выполнении запроса GET к корню сервера. Измените ваш файл main.go так, чтобы он выглядел следующим образом:
С другой стороны, параметр r представляет HTTP-запрос, полученный от клиента. Это то, как мы получаем доступ к данным, отправляемым веб-браузером на сервере. Мы еще не используем его здесь, но мы точно будем использовать его позже.
Затем скомпилируйте и выполните код, который вы только что написали:
Если вы перейдете на http: // localhost: 3000 в своем браузере, вы должны увидеть текст «Hello World!».
Шаблоны в Go
Давайте рассмотрим основы шаблонизации в Go. Если вы знакомы с шаблонами на других языках, это должно быть достаточно просто для понимания.
Шаблоны предоставляют простой способ настроить вывод вашего веб-приложения в зависимости от маршрута без необходимости писать один и тот же код в разных местах. Например, мы можем создать шаблон для панели навигации и использовать его на всех страницах сайта, не дублируя код. Кроме того, мы также получаем возможность добавить некоторую базовую логику на наши веб-страницы.
Импортируйте этот пакет в ваш файл main.go и используйте его следующим образом:
tpl — переменная уровня пакета, которая указывает на определение шаблона из предоставленных файлов. Вызов template.ParseFiles анализирует файл index.html в корне каталога нашего проекта и проверяет его на валидность.
В функции indexHandler мы выполняем созданный ранее шаблон, предоставляя два аргумента: куда мы хотим записать выходные данные и данные, которые мы хотим передать в шаблон.
Добавляем панель навигации на страницу
Замените содержимое тега в вашем файле index.html, как показано ниже:
Затем перезагрузите сервер и обновите ваш браузер. Вы должны увидеть что-то похожее на это:
Работа со статическими файлами
Обратите внимание, что панель навигации, которую мы добавили выше, не имеет стилей, несмотря на тот факт, что мы уже указали их в нашего документа.
Это потому, что путь / фактически совпадает со всеми путями, которые не обрабатываются в другом месте. Поэтому, если вы перейдете на http://localhost:3000/assets/style.css, вы все равно получите домашнюю страницу News Demo вместо файла CSS, потому что маршрут /assets/style.css не был объявлен специально.
Но необходимость объявлять явные обработчики для всех наших статических файлов нереальна и не может масштабироваться. К счастью, мы можем создать один обработчик для обслуживания всех статических ресурсов.
Первое, что нужно сделать, — создать экземпляр объекта файлового сервера, передав каталог, в котором находятся все наши статические файлы:
Далее нам нужно указать нашему маршрутизатору использовать этот объект файлового сервера для всех путей, начинающихся с префикса /assets/ :
Перезагрузите сервер и обновите браузер. Стили должны включиться, как показано ниже:
Создаем роут /search
Давайте создадим роут, который обрабатывает поисковые запросы для новостных статей. Мы будем использовать News API для обработки запросов, поэтому вам нужно зарегистрироваться для получения бесплатного ключа API здесь.
Этот маршрут ожидает два параметра запроса: q представляет запрос пользователя, а page используется для пролистывания результатов. Этот параметр page является необязательным. Если он не включен в URL, мы просто предположим, что номер страницы результатов имеет значение «1».
Добавьте следующий обработчик под indexHandler в ваш файл main.go :
Приведенный выше код извлекает параметры q и page из URL-адреса запроса и выводит их оба в терминал.
Не забудьте импортировать пакеты fmt и net/url сверху:
Теперь перезапустите сервер, введите запрос в поле поиска и проверьте терминал. Вы должны увидеть ваш запрос в терминале, как показано ниже:
Создаём модель данных
Чтобы работать с этими данными в Go, нам нужно сгенерировать структуру, которая отражает данные при декодировании тела ответа. Конечно, вы можете сделать это вручную, но я предпочитаю использовать веб-сайт JSON-to-Go, который делает этот процесс действительно простым. Он генерирует структуру Go (с тегами), которая будет работать для этого JSON.
Все, что вам нужно сделать, это скопировать объект JSON и вставить его в поле, помеченное JSON, затем скопировать вывод и вставить его в свой код. Вот что мы получаем для вышеуказанного объекта JSON:
Как вы, возможно, знаете, Go требует, чтобы все экспортируемые поля в структуре начинались с заглавной буквы. Однако принято представлять поля JSON с помощью camelCase или snake_case, которые не начинаются с заглавной буквы.
Наконец, давайте создадим другой тип структуры для каждого поискового запроса. Добавьте это ниже структуры Results в main.go :
Эта структура представляет собой каждый поисковый запрос, сделанный пользователем. SearchKey — это сам запрос, поле NextPage позволяет пролистывать результаты, TotalPages — общее количество страниц результатов запроса, а Results — текущая страница результатов запроса.
Отправляем запрос по News API и рендерим результаты
Теперь, когда у нас есть модель данных для нашего приложения, давайте продолжим и сделаем запросы к News API, а затем отрендерим результаты на странице.
Сначала объявите новую переменную apiKey под переменной tpl :
Затем используйте её в функции main следующим образом:
Замените два вызова метода fmt.Println() в конце функции searchHandler следующим кодом:
Сначала мы создаем новый экземпляр структуры Search и устанавливаем значение поля SearchKey равным значению параметра URL q в HTTP-запросе.
После этого мы рендерим наш шаблон и передаем переменную search в качестве интерфейса данных. Это позволяет нам получать доступ к данным из объекта JSON в нашем шаблоне, как вы увидите.
Давайте продолжим и отобразим результаты на странице, изменив файл index.html следующим образом. Добавьте это под тегом :
Чтобы получить доступ к полю структуры в шаблоне, мы используем оператор точки. Этот оператор ссылается на объект структуры (в данном случае search ), а затем внутри шаблона мы просто указываем имя поля (как <<.Results>> ).
Перезагрузите сервер, обновите браузер и выполните поиск новостей по популярной теме. Вы должны получить список из 20 результатов на странице, как показано на скрине ниже.
Сохраняем поисковый запрос в инпуте
Обратите внимание, что поисковый запрос исчезает из ввода, когда страница обновляется с результатами. В идеале запрос должен сохраняться до тех пор, пока пользователь не выполнит новый поиск. Вот как Google Search работает, например.
Мы можем легко это исправить, обновив атрибут value тега input в нашем файле index.html следующим образом:
Перезапустите браузер и выполните новый поиск. Поисковый запрос будет сохранен, как показано ниже:
Форматируем дату публикации
Если вы посмотрите на дату в каждой статье, вы увидите, что она плохо читаема. Текущий вывод показывает, как News API возвращает дату публикации статьи. Но мы можем легко изменить это, добавив метод в структуру Article и используя его для форматирования даты вместо использования значения по умолчанию.
Давайте добавим следующий код чуть ниже структуры Article в main.go :
Выводим общее количество результатов
Давайте улучшим пользовательский интерфейс нашего новостного приложения, указав общее количество результатов в верхней части страницы, а затем отобразим сообщение на случай, если по определенному запросу не найдено ни одного результата.
Перезапустите сервер и введите несколько слов в поле поиска, чтобы не было найдено новостей по вашему запросу. На экране должно появиться сообщение «No results found».
После этого сделайте еще один поисковый запрос на этот раз с популярной темой. Количество результатов будет выведено вверху страницы, как показано ниже:
Пагинация
Так как мы отображаем только 20 результатов одновременно, нам нужен способ, чтобы пользователь мог перейти на следующую или предыдущую страницу результатов в любое время.
Сначала добавим кнопку Next внизу результатов, если последняя страница результатов еще не достигнута. Чтобы определить, была ли достигнута последняя страница результатов, создайте этот новый метод ниже объявления структуры Search в main.go :
Пока последняя страница для этого запроса не была достигнута, кнопка Next будет отображаться в нижней части списка результатов.
Теперь перезагрузите сервер и сделайте новый поисковый запрос. У вас должно получиться пролистать результаты, как показано ниже:
Показываем текущую страницу
Вместо того, чтобы отображать только общее количество результатов, найденных для запроса, пользователю также полезно просмотреть общее количество страниц для этого запроса и страницу, на которой он в данный момент находится.
Для этого нам нужно всего лишь изменить наш файл index.html следующим образом:
После того, как вы перезапустите сервер и выполните новый поиск, в верхней части страницы будет указана текущая страница и общее количество страниц вместе с общим количеством результатов.
Деплоим на Heroku
Теперь, когда наше приложение полнофункционально, давайте продолжим и развернем его в Heroku. Зарегистрируйте бесплатную учетную запись, затем перейдите по этой ссылке, чтобы создать новое приложение. Укажите для приложения уникальное имя. Я назвал приложение freshman-news.
Затем следуйте инструкциям здесь, чтобы установить интерфейс командной строки Heroku на свой компьютер. Затем выполните команду heroku login в терминале, чтобы войти в свою учетную запись Heroku.
Убедитесь, что вы инициализировали git-репозиторий для своего проекта. Если нет, запустите команду git init в корне каталога вашего проекта, а затем выполните команду ниже, чтобы установить heroku в качестве удаленного git-репозитория. Замените freshman-news названием вашего приложения.
Затем создайте Procfile в корневом каталоге вашего проекта ( touch Procfile ) и вставьте следующее содержимое:
Перед развертыванием приложения перейдите на вкладку Settings на панели инструментов Heroku и нажмите Reveal Config Vars. Нам нужно установить переменную среды NEWS_API_KEY, чтобы она могла быть передана в бинарный файл при запуске сервера.
Наконец, сделайте коммит своего кода и сделайте пуш в Heroku с помощью следующих команд:
После завершения процесса деплоя вы можете открыть https://название_вашего_приложения.herokuapp.com, чтобы просмотреть и протестировать свой проект.
Заключение
В этой статье мы успешно создали приложение News и обучились основам использования Go для веб-разработки. Мы также изучили, как развернуть готовое приложение в Heroku.
Я надеюсь, что эта статья была полезна для вас. Если у вас есть какие-либо вопросы относительно этого туториала, оставьте комментарий ниже, и я перезвоню вам.