Итак, вы решили сделать новый проект. И проект этот — веб-приложение. Сколько времени уйдёт на создание базового прототипа? Насколько это сложно? Что должен уже со старта уметь современный веб-сайт?
В этой статье мы попробуем набросать boilerplate простейшего веб-приложения со следующей архитектурой:
Что мы покроем:
- настройка dev-окружения в docker-compose.
- создание бэкенда на Flask.
- создание фронтенда на Express.
- сборка JS с помощью Webpack.
- React, Redux и server side rendering.
- очереди задач с RQ.
Введение
Перед разработкой, конечно, сперва нужно определиться, что мы разрабатываем! В качестве модельного приложения для этой статьи я решил сделать примитивный wiki-движок. У нас будут карточки, оформленные в Markdown; их можно будет смотреть и (когда-нибудь в будущем) предлагать правки. Всё это мы оформим в виде одностраничного приложения с server-side rendering (что совершенно необходимо для индексации наших будущих терабайт контента).
Давайте чуть подробнее пройдёмся по компонентам, которые нам для этого понадобятся:
- Клиент. Сделаем одностраничное приложение (т.е. с переходами между страницами посредством AJAX) на весьма распространённой в мире фронтенда связке React+Redux.
- Фронтенд. Сделаем простенький сервер на Express, который будет рендерить наше React-приложение (запрашивая все необходимые данные в бэкенде асинхронно) и выдавать пользователю.
- Бэкенд. Повелитель бизнес-логики, наш бэкенд будет небольшим Flask-приложением. Данные (наши карточки) будем хранить в популярном документном хранилище MongoDB, а для очереди задач и, возможно, в будущем — кэширования будем использовать Redis.
- Воркер. Отдельный контейнер для тяжёлых задач у нас будет запускаться библиотечкой RQ.
Инфраструктура: git
Наверное, про это можно было и не говорить, но, конечно, мы будем вести разработку в git-репозитории.
git init
git remote add origin git@github.com:Saluev/habr-app-demo.git
git commit --allow-empty -m "Initial commit"
git push
(Здесь же стоит сразу наполнить .gitignore
.)
Итоговый проект можно посмотреть на Github. Каждой секции статьи соответствует один коммит (я немало ребейзил, чтобы добиться этого!).
Инфраструктура: docker-compose
Начнём с настройки окружения. При том изобилии компонент, которое у нас имеется, весьма логичным решением для разработки будет использование docker-compose.
Добавим в репозиторий файл docker-compose.yml
следующего содержания:
version: '3'
services:
mongo:
image: "mongo:latest"
redis:
image: "redis:alpine"
backend:
build:
context: .
dockerfile: ./docker/backend/Dockerfile
environment:
- APP_ENV=dev
depends_on:
- mongo
- redis
ports:
- "40001:40001"
volumes:
- .:/code
frontend:
build:
context: .
dockerfile: ./docker/frontend/Dockerfile
environment:
- APP_ENV=dev
- APP_BACKEND_URL=backend:40001
- APP_FRONTEND_PORT=40002
depends_on:
- backend
ports:
- "40002:40002"
volumes:
- ./frontend:/app/src
worker:
build:
context: .
dockerfile: ./docker/worker/Dockerfile
environment:
- APP_ENV=dev
depends_on:
- mongo
- redis
volumes:
- .:/code
Давайте разберём вкратце, что тут происходит.
- Создаётся контейнер MongoDB и контейнер Redis.
- Создаётся контейнер нашего бэкенда (который мы опишем чуть ниже). В него передаётся переменная окружения APP_ENV=dev (мы будем смотреть на неё, чтобы понять, какие настройки Flask загружать), и открывается наружу его порт 40001 (через него в API будет ходить наш браузерный клиент).
- Создаётся контейнер нашего фронтенда. В него тоже прокидываются разнообразные переменные окружения, которые нам потом пригодятся, и открывается порт 40002. Это основной порт нашего веб-приложения: в браузере мы будем заходить на http://localhost:40002.
- Создаётся контейнер нашего воркера. Ему внешние порты не нужны, а нужен только доступ в MongoDB и Redis.
Теперь давайте создадим докерфайлы. Прямо сейчас на Хабре выходит серия переводов прекрасных статей про Docker — за всеми подробностями можно смело обращаться туда.
Начнём с бэкенда.
# docker/backend/Dockerfile
FROM python:stretch
COPY requirements.txt /tmp/
RUN pip install -r /tmp/requirements.txt
ADD . /code
WORKDIR /code
CMD gunicorn -w 1 -b 0.0.0.0:40001 --worker-class gevent backend.server:app
Подразумевается, что мы запускаем через gunicorn Flask-приложение, скрывающееся под именем app
в модуле backend.server
.
Не менее важный docker/backend/.dockerignore
:
.git
.idea
.logs
.pytest_cache
frontend
tests
venv
*.pyc
*.pyo
Воркер в целом аналогичен бэкенду, только вместо gunicorn у нас обычный запуск питонячьего модуля:
# docker/worker/Dockerfile
FROM python:stretch
COPY requirements.txt /tmp/
RUN pip install -r /tmp/requirements.txt
ADD . /code
WORKDIR /code
CMD python -m worker
Мы сделаем всю работу в worker/__main__.py
.
.dockerignore
воркера полностью аналогичен .dockerignore
бэкенда.
Наконец, фронтенд. Про него на Хабре есть целая отдельная статья, но, судя по развернутой дискуссии на StackOverflow и комментариям в духе «Ребят, уже 2018, нормального решения всё ещё нет?» там всё не так просто. Я остановился на таком варианте докерфайла.
# docker/frontend/Dockerfile
FROM node:carbon
WORKDIR /app
# Копируем package.json и package-lock.json и делаем npm install, чтобы зависимости закешировались.
COPY frontend/package*.json ./
RUN npm install
# Наши исходники мы примонтируем в другую папку,
# так что надо задать PATH.
ENV PATH /app/node_modules/.bin:$PATH
# Финальный слой содержит билд нашего приложения.
ADD frontend /app/src
WORKDIR /app/src
RUN npm run build
CMD npm run start
Плюсы:
- всё кешируется как ожидается (на нижнем слое — зависимости, на верхнем — билд нашего приложения);
docker-compose exec frontend npm install --save newDependency
отрабатывает как надо и модифицируетpackage.json
в нашем репозитории (что было бы не так, если бы мы использовали COPY, как многие предлагают). Запускать простоnpm install --save newDependency
вне контейнера в любом случае было бы нежелательно, потому что некоторые зависимости нового пакета могут уже присутствовать и при этом быть собраны под другую платформу (под ту, которая внутри докера, а не под наш рабочий макбук, например), а ещё мы вообще не хотим требовать присутствия Node на разработческой машине. Один Docker, чтобы править ими всеми!
Ну и, конечно, docker/frontend/.dockerignore
:
.git
.idea
.logs
.pytest_cache
backend
worker
tools
node_modules
npm-debug
tests
venv
Итак, наш каркас из контейнеров готов и можно наполнять его содержимым!
Бэкенд: каркас на Flask
Добавим flask
, flask-cors
, gevent
и gunicorn
в requirements.txt
и создадим в backend/server.py
простенький Flask application.
# backend/server.py
import os.path
import flask
import flask_cors
class HabrAppDemo(flask.Flask):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# CORS позволит нашему фронтенду делать запросы к нашему
# бэкенду несмотря на то, что они на разных хостах
# (добавит заголовок Access-Control-Origin в респонсы).
# Подтюним его когда-нибудь потом.
flask_cors.CORS(self)
app = HabrAppDemo("habr-app-demo")
env = os.environ.get("APP_ENV", "dev")
print(f"Starting application in {env} mode")
app.config.from_object(f"backend.{env}_settings")
Мы указали Flask подтягивать настройки из файла backend.{env}_settings
, а значит, нам также потребуется создать (хотя бы пустой) файл backend/dev_settings.py
, чтобы всё взлетело.
Теперь наш бэкенд мы можем официально ПОДНЯТЬ!
habr-app-demo$ docker-compose up backend
...
backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Starting gunicorn 19.9.0
backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Listening at: http://0.0.0.0:40001 (6)
backend_1 | [2019-02-23 10:09:03 +0000] [6] [INFO] Using worker: gevent
backend_1 | [2019-02-23 10:09:03 +0000] [9] [INFO] Booting worker with pid: 9
Двигаемся дальше.
Фронтенд: каркас на Express
Начнём с создания пакета. Создав папку frontend и запустив в ней npm init
, после нескольких бесхитростных вопросов мы получим готовый package.json в духе
{
"name": "habr-app-demo",
"version": "0.0.1",
"description": "This is an app demo for Habr article.",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Saluev/habr-app-demo.git"
},
"author": "Tigran Saluev <tigran@saluev.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/Saluev/habr-app-demo/issues"
},
"homepage": "https://github.com/Saluev/habr-app-demo#readme"
}
В дальнейшем нам вообще не потребуется Node.js на машине разработчика (хотя мы могли и сейчас извернуться и запустить npm init
через Docker, ну да ладно).
В Dockerfile
мы упомянули npm run build
и npm run start
— нужно добавить в package.json
соответствующие команды:
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -4,6 +4,8 @@
"description": "This is an app demo for Habr article.",
"main": "index.js",
"scripts": {
+ "build": "echo 'build'",
+ "start": "node index.js",
"test": "echo "Error: no test specified" && exit 1"
},
"repository": {
Команда build
пока ничего не делает, но она нам ещё пригодится.
Добавим в зависимости Express и создадим в index.js
простое приложение:
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,5 +17,8 @@
"bugs": {
"url": "https://github.com/Saluev/habr-app-demo/issues"
},
- "homepage": "https://github.com/Saluev/habr-app-demo#readme"
+ "homepage": "https://github.com/Saluev/habr-app-demo#readme",
+ "dependencies": {
+ "express": "^4.16.3"
+ }
}
// frontend/index.js
const express = require("express");
app = express();
app.listen(process.env.APP_FRONTEND_PORT);
app.get("*", (req, res) => {
res.send("Hello, world!")
});
Теперь docker-compose up frontend
поднимает наш фронтенд! Более того, на http://localhost:40002 уже должно красоваться классическое “Hello, world”.
Фронтенд: сборка с webpack и React-приложение
Пришло время изобразить в нашем приложении нечто больше, чем plain text. В этой секции мы добавим простейший React-компонент App
и настроим сборку.
При программировании на React очень удобно использовать JSX — диалект JavaScript, расширенный синтаксическими конструкциями вида
render() {
return <MyButton color="blue">{this.props.caption}</MyButton>;
}
Однако, JavaScript-движки не понимают его, поэтому обычно во фронтенд добавляется этап сборки. Специальные компиляторы JavaScript (ага-ага) превращают синтаксический сахар в
уродливый
классический JavaScript, обрабатывают импорты, минифицируют и так далее.
2014 год. apt-cache search java
Итак, простейший React-компонент выглядит очень просто.
// frontend/src/components/app.js
import React, {Component} from 'react'
class App extends Component {
render() {
return <h1>Hello, world!</h1>
}
}
export default App
Он просто выведет на экран наше приветствие более убедительным кеглем.
Добавим файл frontend/src/template.js
, содержащий минимальный HTML-каркас нашего будущего приложения:
// frontend/src/template.js
export default function template(title) {
let page = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>${title}</title>
</head>
<body>
<div id="app"></div>
<script src="/dist/client.js"></script>
</body>
</html>
`;
return page;
}
Добавим и клиентскую точку входа:
// frontend/src/client.js
import React from 'react'
import {render} from 'react-dom'
import App from './components/app'
render(
<App/>,
document.querySelector('#app')
);
Для сборки всей этой красоты нам потребуются:
webpack — модный молодёжный сборщик для JS (хотя я уже три часа не читал статей по фронтенду, так что насчёт моды не уверен);
babel — компилятор для всевозможных примочек вроде JSX, а заодно поставщик полифиллов на все случаи IE.
Если предыдущая итерация фронтенда у вас всё ещё запущена, вам достаточно сделать
docker-compose exec frontend npm install --save
react
react-dom
docker-compose exec frontend npm install --save-dev
webpack
webpack-cli
babel-loader
@babel/core
@babel/polyfill
@babel/preset-env
@babel/preset-react
для установки новых зависимостей. Теперь настроим webpack:
// frontend/webpack.config.js
const path = require("path");
// Конфиг клиента.
clientConfig = {
mode: "development",
entry: {
client: ["./src/client.js", "@babel/polyfill"]
},
output: {
path: path.resolve(__dirname, "../dist"),
filename: "[name].js"
},
module: {
rules: [
{ test: /.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}
};
// Конфиг сервера. Обратите внимание на две вещи:
// 1. target: "node" - без этого упадёт уже на import path.
// 2. складываем в .., а не в ../dist -- нечего пользователям
// видеть код нашего сервера, пусть и скомпилированный!
serverConfig = {
mode: "development",
target: "node",
entry: {
server: ["./index.js", "@babel/polyfill"]
},
output: {
path: path.resolve(__dirname, ".."),
filename: "[name].js"
},
module: {
rules: [
{ test: /.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}
};
module.exports = [clientConfig, serverConfig];
Чтобы заработал babel, нужно сконфигурировать frontend/.babelrc
:
{
"presets": ["@babel/env", "@babel/react"]
}
Наконец, сделаем осмысленной нашу команду npm run build
:
// frontend/package.json
...
"scripts": {
"build": "webpack",
"start": "node /app/server.js",
"test": "echo "Error: no test specified" && exit 1"
},
...
Теперь наш клиент вкупе с пачкой полифиллов и всеми своими зависимостями прогоняется через babel, компилируется и складывается в монолитный минифицированный файлик ../dist/client.js
. Добавим возможность загрузить его как статический файл в наше Express-приложение, а в дефолтном роуте начнём возвращать наш HTML:
// frontend/index.js
// Теперь, когда мы настроили сборку,
// можно по-человечески импортировать.
import express from 'express'
import template from './src/template'
let app = express();
app.use('/dist', express.static('../dist'));
app.get("*", (req, res) => {
res.send(template("Habr demo app"));
});
app.listen(process.env.APP_FRONTEND_PORT);
Успех! Теперь, если мы запустим docker-compose up --build frontend
, мы увидим “Hello, world!” в новой, блестящей обёртке, а если у вас установлено расширение React Developer Tools (Chrome, Firefox) — то ещё и дерево React-компонент в инструментах разработчика:
Бэкенд: данные в MongoDB
Прежде, чем двигаться дальше и вдыхать в наше приложение жизнь, надо сперва её вдохнуть в бэкенд. Кажется, мы собирались хранить размеченные в Markdown карточки — пора это сделать.
В то время, как существуют ORM для MongoDB на питоне, я считаю использование ORM практикой порочной и оставляю изучение соответствующих решений на ваше усмотрение. Вместо этого сделаем простенький класс для карточки и сопутствующий DAO:
# backend/storage/card.py
import abc
from typing import Iterable
class Card(object):
def __init__(self, id: str = None, slug: str = None, name: str = None, markdown: str = None, html: str = None):
self.id = id
self.slug = slug # человекочитаемый идентификатор карточки
self.name = name
self.markdown = markdown
self.html = html
class CardDAO(object, metaclass=abc.ABCMeta):
@abc.abstractmethod
def create(self, card: Card) -> Card:
pass
@abc.abstractmethod
def update(self, card: Card) -> Card:
pass
@abc.abstractmethod
def get_all(self) -> Iterable[Card]:
pass
@abc.abstractmethod
def get_by_id(self, card_id: str) -> Card:
pass
@abc.abstractmethod
def get_by_slug(self, slug: str) -> Card:
pass
class CardNotFound(Exception):
pass
(Если вы до сих пор не используете аннотации типов в Python, обязательно гляньте эти статьи!)
Теперь создадим реализацию интерфейса CardDAO
, принимающую на вход объект Database
из pymongo
(да-да, время добавить pymongo
в requirements.txt
):
# backend/storage/card_impl.py
from typing import Iterable
import bson
import bson.errors
from pymongo.collection import Collection
from pymongo.database import Database
from backend.storage.card import Card, CardDAO, CardNotFound
class MongoCardDAO(CardDAO):
def __init__(self, mongo_database: Database):
self.mongo_database = mongo_database
# Очевидно, slug должны быть уникальны.
self.collection.create_index("slug", unique=True)
@property
def collection(self) -> Collection:
return self.mongo_database["cards"]
@classmethod
def to_bson(cls, card: Card):
# MongoDB хранит документы в формате BSON. Здесь
# мы должны сконвертировать нашу карточку в BSON-
# сериализуемый объект, что бы в ней ни хранилось.
result = {
k: v
for k, v in card.__dict__.items()
if v is not None
}
if "id" in result:
result["_id"] = bson.ObjectId(result.pop("id"))
return result
@classmethod
def from_bson(cls, document) -> Card:
# С другой стороны, мы хотим абстрагировать весь
# остальной код от того факта, что мы храним карточки
# в монге. Но при этом id будет неизбежно везде
# использоваться, так что сконвертируем-ка его в строку.
document["id"] = str(document.pop("_id"))
return Card(**document)
def create(self, card: Card) -> Card:
card.id = str(self.collection.insert_one(self.to_bson(card)).inserted_id)
return card
def update(self, card: Card) -> Card:
card_id = bson.ObjectId(card.id)
self.collection.update_one({"_id": card_id}, {"$set": self.to_bson(card)})
return card
def get_all(self) -> Iterable[Card]:
for document in self.collection.find():
yield self.from_bson(document)
def get_by_id(self, card_id: str) -> Card:
return self._get_by_query({"_id": bson.ObjectId(card_id)})
def get_by_slug(self, slug: str) -> Card:
return self._get_by_query({"slug": slug})
def _get_by_query(self, query) -> Card:
document = self.collection.find_one(query)
if document is None:
raise CardNotFound()
return self.from_bson(document)
Время прописать конфигурацию монги в настройки бэкенда. Мы незамысловато назвали наш контейнер с монгой mongo
, так что MONGO_HOST = "mongo"
:
--- a/backend/dev_settings.py
+++ b/backend/dev_settings.py
@@ -0,0 +1,3 @@
+MONGO_HOST = "mongo"
+MONGO_PORT = 27017
+MONGO_DATABASE = "core"
Теперь надо создать MongoCardDAO
и дать Flask-приложению к нему доступ. Хотя сейчас у нас очень простая иерархия объектов (настройки → клиент pymongo → база данных pymongo → MongoCardDAO
), давайте сразу создадим централизованный царь-компонент, делающий dependency injection (он пригодится нам снова, когда мы будем делать воркер и tools).
# backend/wiring.py
import os
from pymongo import MongoClient
from pymongo.database import Database
import backend.dev_settings
from backend.storage.card import CardDAO
from backend.storage.card_impl import MongoCardDAO
class Wiring(object):
def __init__(self, env=None):
if env is None:
env = os.environ.get("APP_ENV", "dev")
self.settings = {
"dev": backend.dev_settings,
# (добавьте сюда настройки других
# окружений, когда они появятся!)
}[env]
# С ростом числа компонент этот код будет усложняться.
# В будущем вы можете сделать тут такой DI, какой захотите.
self.mongo_client: MongoClient = MongoClient(
host=self.settings.MONGO_HOST,
port=self.settings.MONGO_PORT)
self.mongo_database: Database = self.mongo_client[self.settings.MONGO_DATABASE]
self.card_dao: CardDAO = MongoCardDAO(self.mongo_database)
Время добавить новый роут в Flask-приложение и наслаждаться видом!
# backend/server.py
import os.path
import flask
import flask_cors
from backend.storage.card import CardNotFound
from backend.wiring import Wiring
env = os.environ.get("APP_ENV", "dev")
print(f"Starting application in {env} mode")
class HabrAppDemo(flask.Flask):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
flask_cors.CORS(self)
self.wiring = Wiring(env)
self.route("/api/v1/card/<card_id_or_slug>")(self.card)
def card(self, card_id_or_slug):
try:
card = self.wiring.card_dao.get_by_slug(card_id_or_slug)
except CardNotFound:
try:
card = self.wiring.card_dao.get_by_id(card_id_or_slug)
except (CardNotFound, ValueError):
return flask.abort(404)
return flask.jsonify({
k: v
for k, v in card.__dict__.items()
if v is not None
})
app = HabrAppDemo("habr-app-demo")
app.config.from_object(f"backend.{env}_settings")
Перезапускаем командой docker-compose up --build backend
:
Упс… ох, точно. Нам же нужно добавить контент! Заведём папку tools и сложим в неё скриптик, добавляющий одну тестовую карточку:
# tools/add_test_content.py
from backend.storage.card import Card
from backend.wiring import Wiring
wiring = Wiring()
wiring.card_dao.create(Card(
slug="helloworld",
name="Hello, world!",
markdown="""
This is a hello-world page.
"""))
Команда docker-compose exec backend python -m tools.add_test_content
наполнит нашу монгу контентом изнутри контейнера с бэкендом.
Успех! Теперь время поддержать это на фронтенде.
Фронтенд: Redux
Теперь мы хотим сделать роут /card/:id_or_slug
, по которому будет открываться наше React-приложение, подгружать данные карточки из API и как-нибудь её нам показывать. И здесь начинается, пожалуй, самое сложное, ведь мы хотим, чтобы сервер сразу отдавал нам HTML с содержимым карточки, пригодным для индексации, но при этом чтобы приложение при навигации между карточками получало все данные в виде JSON из API, а страничка не перегружалась. И чтобы всё это — без копипасты!
Начнём с добавления Redux. Redux — JavaScript-библиотека для хранения состояния. Идея в том, чтобы вместо тысячи неявных состояний, изменяемых вашими компонентами при пользовательских действиях и других интересных событиях, иметь одно централизованное состояние, а любое изменение его производить через централизованный механизм действий. Так, если раньше для навигации мы сперва включали гифку загрузки, потом делали запрос через AJAX и, наконец, в success-коллбеке прописывали обновление нужных частей страницы, то в Redux-парадигме нам предлагается отправить действие “изменить контент на гифку с анимацией”, которое изменит глобальное состояние так, что одна из ваших компонент выкинет прежний контент и поставит анимацию, потом сделать запрос, а в его success-коллбеке отправить ещё одно действие, “изменить контент на подгруженный”. В общем, сейчас мы это сами увидим.
Начнём с установки новых зависимостей в наш контейнер.
docker-compose exec frontend npm install --save
redux
react-redux
redux-thunk
redux-devtools-extension
Первое — собственно, Redux, второе — специальная библиотека для скрещивания React и Redux (written by mating experts), третье — очень нужная штука, необходимость который неплохо обоснована в её же README, и, наконец, четвёртое — библиотечка, необходимая для работы Redux DevTools Extension.
Начнём с бойлерплейтного Redux-кода: создания редьюсера, который ничего не делает, и инициализации состояния.
// frontend/src/redux/reducers.js
export default function root(state = {}, action) {
return state;
}
// frontend/src/redux/configureStore.js
import {createStore, applyMiddleware} from "redux";
import thunkMiddleware from "redux-thunk";
import {composeWithDevTools} from "redux-devtools-extension";
import rootReducer from "./reducers";
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware)),
);
}
Наш клиент немного видоизменяется, морально готовясь к работе с Redux:
// frontend/src/client.js
import React from 'react'
import {render} from 'react-dom'
import {Provider} from 'react-redux'
import App from './components/app'
import configureStore from './redux/configureStore'
// Нужно создать то самое централизованное хранилище...
const store = configureStore();
render(
// ... и завернуть приложение в специальный компонент,
// умеющий с ним работать
<Provider store={store}>
<App/>
</Provider>,
document.querySelector('#app')
);
Теперь мы можем запустить docker-compose up –build frontend, чтобы убедиться, что ничего не сломалось, а в Redux DevTools появилось наше примитивное состояние:
Фронтенд: страница карточки
Прежде, чем сделать страницы с SSR, надо сделать страницы без SSR! Давайте наконец воспользуемся нашим гениальным API для доступа к карточкам и сверстаем страницу карточки на фронтенде.
Время воспользоваться интеллектом и задизайнить структуру нашего состояния. Материалов на эту тему довольно много, так что предлагаю интеллектом не злоупотреблять и остановится на простом. Например, таком:
{
"page": {
"type": "card", // что за страница открыта
// следующие свойства должны быть только при type=card:
"cardSlug": "...", // что за карточка открыта
"isFetching": false, // происходит ли сейчас запрос к API
"cardData": {...}, // данные карточки (если уже получены)
// ...
},
// ...
}
Заведём компонент «карточка», принимающий в качестве props содержимое cardData (оно же — фактически содержимое нашей карточки в mongo):
// frontend/src/components/card.js
import React, {Component} from 'react';
class Card extends Component {
componentDidMount() {
document.title = this.props.name
}
render() {
const {name, html} = this.props;
return (
<div>
<h1>{name}</h1>
<!--Да-да, добавить HTML в React не так-то просто!-->
<div dangerouslySetInnerHTML={{__html: html}}/>
</div>
);
}
}
export default Card;
Теперь заведём компонент для всей страницы с карточкой. Он будет ответственен за то, чтобы достать нужные данные из API и передать их в Card. А фетчинг данных мы сделаем React-Redux way.
Для начала создадим файлик frontend/src/redux/actions.js
и создадим действие, которые достаёт из API содержимое карточки, если ещё не:
export function fetchCardIfNeeded() {
return (dispatch, getState) => {
let state = getState().page;
if (state.cardData === undefined || state.cardData.slug !== state.cardSlug) {
return dispatch(fetchCard());
}
};
}
Действие fetchCard
, которое, собственно, делает фетч, чуть-чуть посложнее:
function fetchCard() {
return (dispatch, getState) => {
// Сперва даём состоянию понять, что мы ждём карточку.
// Наши компоненты после этого могут, например,
// включить характерную анимацию загрузки.
dispatch(startFetchingCard());
// Формируем запрос к API.
let url = apiPath() + "/card/" + getState().page.cardSlug;
// Фетчим, обрабатываем, даём состоянию понять, что
// данные карточки уже доступны. Здесь, конечно, хорошо
// бы добавить обработку ошибок.
return fetch(url)
.then(response => response.json())
.then(json => dispatch(finishFetchingCard(json)));
};
// Кстати, именно redux-thunk позволяет нам
// использовать в качестве действий лямбды.
}
function startFetchingCard() {
return {
type: START_FETCHING_CARD
};
}
function finishFetchingCard(json) {
return {
type: FINISH_FETCHING_CARD,
cardData: json
};
}
function apiPath() {
// Эта функция здесь неспроста. Когда мы сделаем server-side
// rendering, путь к API будет зависеть от окружения - из
// контейнера с фронтендом надо будет стучать не в localhost,
// а в backend.
return "http://localhost:40001/api/v1";
}
Ох, у нас появилось действие, которое ЧТО-ТО ДЕЛАЕТ! Это надо поддержать в редьюсере:
// frontend/src/redux/reducers.js
import {
START_FETCHING_CARD,
FINISH_FETCHING_CARD
} from "./actions";
export default function root(state = {}, action) {
switch (action.type) {
case START_FETCHING_CARD:
return {
...state,
page: {
...state.page,
isFetching: true
}
};
case FINISH_FETCHING_CARD:
return {
...state,
page: {
...state.page,
isFetching: false,
cardData: action.cardData
}
}
}
return state;
}
(Обратите внимание на сверхмодный синтаксис для клонирования объекта с изменением отдельных полей.)
Теперь, когда вся логика унесена в Redux actions, сама компонента CardPage
будет выглядеть сравнительно просто:
// frontend/src/components/cardPage.js
import React, {Component} from 'react';
import {connect} from 'react-redux'
import {fetchCardIfNeeded} from '../redux/actions'
import Card from './card'
class CardPage extends Component {
componentWillMount() {
// Это событие вызывается, когда React собирается
// отрендерить наш компонент. К моменту рендеринга нам уже
// уже желательно знать, показывать ли заглушку "данные
// загружаются" или рисовать карточку, поэтому мы вызываем
// наше царь-действие здесь. Ещё одна причина - этот метод
// вызывается также при рендеринге компонент в HTML функцией
// renderToString, которую мы будем использовать для SSR.
this.props.dispatch(fetchCardIfNeeded())
}
render() {
const {isFetching, cardData} = this.props;
return (
<div>
{isFetching && <h2>Loading...</h2>}
{cardData && <Card {...cardData}/>}
</div>
);
}
}
// Поскольку этой компоненте нужен доступ к состоянию, ей нужно
// его обеспечить. Именно для этого мы подключили в зависимости
// пакет react-redux. Помимо содержимого page ей будет передана
// функция dispatch, позволяющая выполнять действия.
function mapStateToProps(state) {
const {page} = state;
return page;
}
export default connect(mapStateToProps)(CardPage);
Добавим простенькую обработку page.type в наш корневой компонент App:
// frontend/src/components/app.js
import React, {Component} from 'react'
import {connect} from "react-redux";
import CardPage from "./cardPage"
class App extends Component {
render() {
const {pageType} = this.props;
return (
<div>
{pageType === "card" && <CardPage/>}
</div>
);
}
}
function mapStateToProps(state) {
const {page} = state;
const {type} = page;
return {
pageType: type
};
}
export default connect(mapStateToProps)(App);
И теперь остался последний момент — надо как-то инициализировать page.type
и page.cardSlug
в зависимости от URL страницы.
Но в этой статье ещё много разделов, мы же не можем сделать качественное решение прямо сейчас. Давайте пока что сделаем это как-нибудь глупо. Вот прям совсем глупо. Например, регуляркой при инициализации приложения!
// frontend/src/client.js
import React from 'react'
import {render} from 'react-dom'
import {Provider} from 'react-redux'
import App from './components/app'
import configureStore from './redux/configureStore'
let initialState = {
page: {
type: "home"
}
};
const m = /^/card/([^/]+)$/.exec(location.pathname);
if (m !== null) {
initialState = {
page: {
type: "card",
cardSlug: m[1]
},
}
}
const store = configureStore(initialState);
render(
<Provider store={store}>
<App/>
</Provider>,
document.querySelector('#app')
);
Теперь мы можем пересобрать фронтенд с помощью docker-compose up --build frontend
, чтобы насладиться нашей карточкой helloworld
…
Так, секундочку… а где же наш контент? Ох, да мы ведь забыли распарсить Markdown!
Воркер: RQ
Парсинг Markdown и генерация HTML для карточки потенциально неограниченного размера — типичная «тяжёлая» задача, которую вместо того, чтобы решать прямо на бэкенде при сохранении изменений, обычно ставят в очередь и исполняют на отдельных машинах — воркерах.
Есть много опенсорсных реализаций очередей задач; мы возьмём Redis и простенькую библиотечку RQ (Redis Queue), которая передаёт параметры задач в формате pickle и сама организует нам спаунинг процессов для их обработки.
Время добавить редис в зависимости, настройки и вайринг!
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,5 @@ flask-cors
gevent
gunicorn
pymongo
+redis
+rq
--- a/backend/dev_settings.py
+++ b/backend/dev_settings.py
@@ -1,3 +1,7 @@
MONGO_HOST = "mongo"
MONGO_PORT = 27017
MONGO_DATABASE = "core"
+REDIS_HOST = "redis"
+REDIS_PORT = 6379
+REDIS_DB = 0
+TASK_QUEUE_NAME = "tasks"
--- a/backend/wiring.py
+++ b/backend/wiring.py
@@ -2,6 +2,8 @@ import os
from pymongo import MongoClient
from pymongo.database import Database
+import redis
+import rq
import backend.dev_settings
from backend.storage.card import CardDAO
@@ -21,3 +23,11 @@ class Wiring(object):
port=self.settings.MONGO_PORT)
self.mongo_database: Database = self.mongo_client[self.settings.MONGO_DATABASE]
self.card_dao: CardDAO = MongoCardDAO(self.mongo_database)
+
+ self.redis: redis.Redis = redis.StrictRedis(
+ host=self.settings.REDIS_HOST,
+ port=self.settings.REDIS_PORT,
+ db=self.settings.REDIS_DB)
+ self.task_queue: rq.Queue = rq.Queue(
+ name=self.settings.TASK_QUEUE_NAME,
+ connection=self.redis)
Немного бойлерплейтного кода для воркера.
# worker/__main__.py
import argparse
import uuid
import rq
import backend.wiring
parser = argparse.ArgumentParser(description="Run worker.")
# Удобно иметь флаг, заставляющий воркер обработать все задачи
# и выключиться. Вдвойне удобно, что такой режим уже есть в rq.
parser.add_argument(
"--burst",
action="store_const",
const=True,
default=False,
help="enable burst mode")
args = parser.parse_args()
# Нам нужны настройки и подключение к Redis.
wiring = backend.wiring.Wiring()
with rq.Connection(wiring.redis):
w = rq.Worker(
queues=[wiring.settings.TASK_QUEUE_NAME],
# Если мы захотим запускать несколько воркеров в разных
# контейнерах, им потребуются уникальные имена.
name=uuid.uuid4().hex)
w.work(burst=args.burst)
Для самого парсинга подключим библиотечку mistune и напишем простенькую функцию:
# backend/tasks/parse.py
import mistune
from backend.storage.card import CardDAO
def parse_card_markup(card_dao: CardDAO, card_id: str):
card = card_dao.get_by_id(card_id)
card.html = _parse_markdown(card.markdown)
card_dao.update(card)
_parse_markdown = mistune.Markdown(escape=True, hard_wrap=False)
Логично: нам нужен CardDAO
, чтобы получить исходники карточки и чтобы сохранить результат. Но объект, содержащий подключение к внешнему хранилищу, нельзя сериализовать через pickle — а значит, эту таску нельзя сразу взять и поставить в очередь RQ. По-хорошему нам нужно создать Wiring
на стороне воркера и прокидывать его во все таски… Давайте сделаем это:
--- a/worker/__main__.py
+++ b/worker/__main__.py
@@ -2,6 +2,7 @@ import argparse
import uuid
import rq
+from rq.job import Job
import backend.wiring
@@ -16,8 +17,23 @@ args = parser.parse_args()
wiring = backend.wiring.Wiring()
+
+class JobWithWiring(Job):
+
+ @property
+ def kwargs(self):
+ result = dict(super().kwargs)
+ result["wiring"] = backend.wiring.Wiring()
+ return result
+
+ @kwargs.setter
+ def kwargs(self, value):
+ super().kwargs = value
+
+
with rq.Connection(wiring.redis):
w = rq.Worker(
queues=[wiring.settings.TASK_QUEUE_NAME],
- name=uuid.uuid4().hex)
+ name=uuid.uuid4().hex,
+ job_class=JobWithWiring)
w.work(burst=args.burst)
Мы объявили свой класс джобы, прокидывающий вайринг в качестве дополнительного kwargs-аргумента во все таски. (Обратите внимание, что он создаёт каждый раз НОВЫЙ вайринг, потому что некоторые клиенты нельзя создавать перед форком, который происходит внутри RQ перед началом обработки задачи.) Чтобы все наши таски не стали зависеть от вайринга — то есть от ВСЕХ наших объектов, — давайте сделаем декоратор, который будет доставать из вайринга только нужное:
# backend/tasks/task.py
import functools
from typing import Callable
from backend.wiring import Wiring
def task(func: Callable):
# Достаём имена аргументов функции:
varnames = func.__code__.co_varnames
@functools.wraps(func)
def result(*args, **kwargs):
# Достаём вайринг. Используем .pop(), потому что мы не
# хотим, чтобы у тасок был доступ ко всему вайрингу.
wiring: Wiring = kwargs.pop("wiring")
wired_objects_by_name = wiring.__dict__
for arg_name in varnames:
if arg_name in wired_objects_by_name:
kwargs[arg_name] = wired_objects_by_name[arg_name]
# Здесь могло бы быть получение объекта из вайринга по
# аннотации типа аргумента, но как-нибудь в другой раз.
return func(*args, **kwargs)
return result
Добавляем декоратор к нашей таске и радуемся жизни:
import mistune
from backend.storage.card import CardDAO
from backend.tasks.task import task
@task
def parse_card_markup(card_dao: CardDAO, card_id: str):
card = card_dao.get_by_id(card_id)
card.html = _parse_markdown(card.markdown)
card_dao.update(card)
_parse_markdown = mistune.Markdown(escape=True, hard_wrap=False)
Радуемся жизни? Тьфу, я хотел сказать, запускаем воркер:
$ docker-compose up worker
...
Creating habr-app-demo_worker_1 ... done
Attaching to habr-app-demo_worker_1
worker_1 | 17:21:03 RQ worker 'rq:worker:49a25686acc34cdfa322feb88a780f00' started, version 0.13.0
worker_1 | 17:21:03 *** Listening on tasks...
worker_1 | 17:21:03 Cleaning registries for queue: tasks
Ииии… он ничего не делает! Конечно, ведь мы не ставили ни одной таски!
Давайте перепишем нашу тулзу, которая создаёт тестовую карточку, чтобы она: а) не падала, если карточка уже создана (как в нашем случае); б) ставила таску на парсинг маркдауна.
# tools/add_test_content.py
from backend.storage.card import Card, CardNotFound
from backend.tasks.parse import parse_card_markup
from backend.wiring import Wiring
wiring = Wiring()
try:
card = wiring.card_dao.get_by_slug("helloworld")
except CardNotFound:
card = wiring.card_dao.create(Card(
slug="helloworld",
name="Hello, world!",
markdown="""
This is a hello-world page.
"""))
# Да, тут нужен card_dao.get_or_create, но
# эта статья и так слишком длинная!
wiring.task_queue.enqueue_call(
parse_card_markup, kwargs={"card_id": card.id})
Тулзу теперь можно запускать не только на backend, но и на worker. В принципе, сейчас нам нет разницы. Запускаем docker-compose exec worker python -m tools.add_test_content
и в соседней вкладке терминала видим чудо — воркер ЧТО-ТО СДЕЛАЛ!
worker_1 | 17:34:26 tasks: backend.tasks.parse.parse_card_markup(card_id='5c715dd1e201ce000c6a89fa') (613b53b1-726b-47a4-9c7b-97cad26da1a5)
worker_1 | 17:34:27 tasks: Job OK (613b53b1-726b-47a4-9c7b-97cad26da1a5)
worker_1 | 17:34:27 Result is kept for 500 seconds
Пересобрав контейнер с бэкендом, мы наконец можем увидеть контент нашей карточки в браузере:
Фронтенд: навигация
Прежде, чем мы перейдём к SSR, нам нужно сделать всю нашу возню с React хоть сколько-то осмысленной и сделать наше single page application действительно single page. Давайте обновим нашу тулзу, чтобы создавалось две (НЕ ОДНА, А ДВЕ! МАМА, Я ТЕПЕРЬ БИГ ДАТА ДЕВЕЛОПЕР!) карточки, ссылающиеся друг на друга, и потом займёмся навигацией между ними.
Скрытый текст
# tools/add_test_content.py
def create_or_update(card):
try:
card.id = wiring.card_dao.get_by_slug(card.slug).id
card = wiring.card_dao.update(card)
except CardNotFound:
card = wiring.card_dao.create(card)
wiring.task_queue.enqueue_call(
parse_card_markup, kwargs={"card_id": card.id})
create_or_update(Card(
slug="helloworld",
name="Hello, world!",
markdown="""
This is a hello-world page. It can't really compete with the [demo page](demo).
"""))
create_or_update(Card(
slug="demo",
name="Demo Card!",
markdown="""
Hi there, habrovchanin. You've probably got here from the awkward ["Hello, world" card](helloworld).
Well, **good news**! Finally you are looking at a **really cool card**!
"""
))
Теперь мы можем ходить по ссылкам и созерцать, как каждый раз наше чудесное приложение перезагружается. Хватит это терпеть!
Сперва навесим свой обработчик на клики по ссылкам. Поскольку HTML со ссылками у нас приходит с бэкенда, а приложение у нас на React, потребуется небольшой React-специфический фокус.
// frontend/src/components/card.js
class Card extends Component {
componentDidMount() {
document.title = this.props.name
}
navigate(event) {
// Это обработчик клика по всему нашему контенту. Поэтому
// на каждый клик надо сперва проверить, по ссылке ли он.
if (event.target.tagName === 'A'
&& event.target.hostname === window.location.hostname) {
// Отменяем стандартное поведение браузера
event.preventDefault();
// Запускаем своё действие для навигации
this.props.dispatch(navigate(event.target));
}
}
render() {
const {name, html} = this.props;
return (
<div>
<h1>{name}</h1>
<div
dangerouslySetInnerHTML={{__html: html}}
onClick={event => this.navigate(event)}
/>
</div>
);
}
}
Поскольку вся логика с подгрузкой карточки у нас в компоненте CardPage
, в самом действии (изумительно!) не нужно предпринимать никаких действий:
export function navigate(link) {
return {
type: NAVIGATE,
path: link.pathname
}
}
Добавляем глупенький редьюсер под это дело:
// frontend/src/redux/reducers.js
import {
START_FETCHING_CARD,
FINISH_FETCHING_CARD,
NAVIGATE
} from "./actions";
function navigate(state, path) {
// Здесь мог бы быть react-router, но он больно сложный!
// (И ещё его очень трудно скрестить с SSR.)
let m = /^/card/([^/]+)$/.exec(path);
if (m !== null) {
return {
...state,
page: {
type: "card",
cardSlug: m[1],
isFetching: true
}
};
}
return state
}
export default function root(state = {}, action) {
switch (action.type) {
case START_FETCHING_CARD:
return {
...state,
page: {
...state.page,
isFetching: true
}
};
case FINISH_FETCHING_CARD:
return {
...state,
page: {
...state.page,
isFetching: false,
cardData: action.cardData
}
};
case NAVIGATE:
return navigate(state, action.path)
}
return state;
}
Поскольку теперь состояние нашего приложения может изменяться, в CardPage
нужно добавить метод componentDidUpdate
, идентичный уже добавленному нами componentWillMount
. Теперь после обновления свойств CardPage
(например, свойства cardSlug
при навигации) тоже будет запрашиваться контент карточки с бэкенда (componentWillMount
делал это только при инициализации компоненты).
Вжух, docker-compose up --build frontend
и у нас рабочая навигация!
Внимательный читатель обратит внимание, что URL страницы не будет изменяться при навигации между карточками — даже на скриншоте мы видим Hello, world-карточку по адресу demo-карточки. Соответственно, навигация вперёд-назад тоже отвалилась. Давайте сразу добавим немного чёрной магии с history, чтобы починить это!
Самое простое, что можно сделать — добавить в действие navigate
вызов history.pushState
.
export function navigate(link) {
history.pushState(null, "", link.href);
return {
type: NAVIGATE,
path: link.pathname
}
}
Теперь при переходах по ссылкам URL в адресной строке браузера будет реально меняться. Однако, кнопка «Назад» сломается!
Чтобы всё заработало, нам надо слушать событие popstate
объекта window
. Причём, если мы захотим при этом событии делать навигацию назад так же, как и вперёд (то есть через dispatch(navigate(...))
), то придётся в функцию navigate
добавить специальный флаг «не делай pushState
» (иначе всё разломается ещё сильнее!). Кроме того, чтобы различать «наши» состояния, нам стоит воспользоваться способностью pushState
сохранять метаданные. Тут много магии и дебага, поэтому перейдём сразу к коду! Вот как станет выглядеть App:
// frontend/src/components/app.js
class App extends Component {
componentDidMount() {
// Наше приложение только загрузилось -- надо сразу
// пометить текущее состояние истории как "наше".
history.replaceState({
pathname: location.pathname,
href: location.href
}, "");
// Добавляем обработчик того самого события.
window.addEventListener("popstate", event => this.navigate(event));
}
navigate(event) {
// Триггеримся только на "наше" состояние, иначе пользователь
// не сможет вернуться по истории на тот сайт, с которого к
// нам пришёл (or is it a good thing?..)
if (event.state && event.state.pathname) {
event.preventDefault();
event.stopPropagation();
// Диспатчим наше действие в режиме "не делай pushState".
this.props.dispatch(navigate(event.state, true));
}
}
render() {
// ...
}
}
А вот как — действие navigate:
// frontend/src/redux/actions.js
export function navigate(link, dontPushState) {
if (!dontPushState) {
history.pushState({
pathname: link.pathname,
href: link.href
}, "", link.href);
}
return {
type: NAVIGATE,
path: link.pathname
}
}
Вот теперь история заработает.
Ну и последний штрих: раз уж у нас теперь есть действие navigate
, почему бы нам не отказаться от лишнего кода в клиенте, вычисляющего начальное состояние? Мы ведь можем просто вызвать navigate в текущий location:
--- a/frontend/src/client.js
+++ b/frontend/src/client.js
@@ -3,23 +3,16 @@ import {render} from 'react-dom'
import {Provider} from 'react-redux'
import App from './components/app'
import configureStore from './redux/configureStore'
+import {navigate} from "./redux/actions";
let initialState = {
page: {
type: "home"
}
};
-const m = /^/card/([^/]+)$/.exec(location.pathname);
-if (m !== null) {
- initialState = {
- page: {
- type: "card",
- cardSlug: m[1]
- },
- }
-}
const store = configureStore(initialState);
+store.dispatch(navigate(location));
Копипаста уничтожена!
Фронтенд: server-side rendering
Пришло время для нашей главной (на мой взгляд) фишечки — SEO-дружелюбия. Чтобы поисковики могли индексировать наш контент, полностью создаваемый динамически в React-компонентах, нам нужно уметь выдавать им результат рендеринга React, и ещё и научиться потом делать этот результат снова интерактивным.
Общая схема простая. Первое: в наш HTML-шаблон нам надо воткнуть HTML, сгенерированный нашим React-компонентом App
. Этот HTML будут видеть поисковые движки (и браузеры с выключенным JS, хе-хе). Второе: в шаблон надо добавить тег <script>
, сохраняющий куда-нибудь (например, в объект window
) дамп состояния, из которого отрендерился этот HTML. Тогда мы сможем сразу инициализировать наше приложение на стороне клиента этим состоянием и показывать что надо (мы даже можем применить hydrate к сгенерированному HTML, чтобы не создавать DOM tree приложения заново).
Начнём с написания функции, возвращающей отрендеренный HTML и итоговое состояние.
// frontend/src/server.js
import "@babel/polyfill"
import React from 'react'
import {renderToString} from 'react-dom/server'
import {Provider} from 'react-redux'
import App from './components/app'
import {navigate} from "./redux/actions";
import configureStore from "./redux/configureStore";
export default function render(initialState, url) {
// Создаём store, как и на клиенте.
const store = configureStore(initialState);
store.dispatch(navigate(url));
let app = (
<Provider store={store}>
<App/>
</Provider>
);
// Оказывается, в реакте уже есть функция рендеринга в строку!
// Автор, ну и зачем ты десять разделов пудрил мне мозги?
let content = renderToString(app);
let preloadedState = store.getState();
return {content, preloadedState};
};
Добавим в наш шаблон новые аргументы и логику, о которой мы говорили выше:
// frontend/src/template.js
function template(title, initialState, content) {
let page = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>${title}</title>
</head>
<body>
<div id="app">${content}</div>
<script>
window.__STATE__ = ${JSON.stringify(initialState)}
</script>
<script src="/dist/client.js"></script>
</body>
</html>
`;
return page;
}
module.exports = template;
Немного сложнее становится наш Express-сервер:
// frontend/index.js
app.get("*", (req, res) => {
const initialState = {
page: {
type: "home"
}
};
const {content, preloadedState} = render(initialState, {pathname: req.url});
res.send(template("Habr demo app", preloadedState, content));
});
Зато клиент — проще:
// frontend/src/client.js
import React from 'react'
import {hydrate} from 'react-dom'
import {Provider} from 'react-redux'
import App from './components/app'
import configureStore from './redux/configureStore'
import {navigate} from "./redux/actions";
// Больше не надо задавать начальное состояние и дёргать навигацию!
const store = configureStore(window.__STATE__);
// render сменился на hydrate. hydrate возьмёт уже существующее
// DOM tree, провалидирует и навесит где надо ивент хендлеры.
hydrate(
<Provider store={store}>
<App/>
</Provider>,
document.querySelector('#app')
);
Дальше нужно вычистить ошибки кроссплатформенности вроде «history is not defined». Для этого добавим простую (пока что) фунцию куда-нибудь в utility.js
.
// frontend/src/utility.js
export function isServerSide() {
// Вы можете возмутиться, что в браузере не будет process,
// но компиляция с полифиллами как-то разруливает этот вопрос.
return process.env.APP_ENV !== undefined;
}
Дальше будет какое-то количество рутинных изменений, которые я не буду тут приводить (но их можно посмотреть в соответствующем коммите). В итоге наше React-приложение сможет рендериться и в браузере, и на сервере.
Работает! Но есть, как говорится, один нюанс…
LOADING? Всё, что увидит Google на моём супер-крутом модном сервисе — это LOADING?!
Что ж, кажется, вся наша асинхронщина сыграла против нас. Теперь нам нужен способ дать серверу понять, что ответа от бэкенда с контентом карточки нужно дождаться, прежде чем рендерить React-приложение в строку и отправлять клиенту. И желательно, чтобы способ этот был достаточно общий.
Здесь может быть много решений. Один из подходов — описать в отдельном файле, для каких путей какие данные нужно зафетчить, и сделать это перед тем, как рендерить приложение (статья). У этого решения много плюсов. Оно простое, оно явное и оно работает.
В качестве эксперимента (должен же быть в статье хоть где-то ориджинал контент!) я предлагаю другую схему. Давайте каждый раз, когда мы запускаем что-то асинхронное, чего надо дожидаться, добавлять соответствующий промис (например, тот, который возвращает fetch) куда-нибудь в наше состояние. Так у нас будет место, где всегда можно проверить, всё ли скачалось.
Добавим два новых действия.
// frontend/src/redux/actions.js
function addPromise(promise) {
return {
type: ADD_PROMISE,
promise: promise
};
}
function removePromise(promise) {
return {
type: REMOVE_PROMISE,
promise: promise,
};
}
Первое будем вызывать, когда запустили фетч, второе — в конце его .then()
.
Теперь добавим их обработку в редьюсер:
// frontend/src/redux/reducers.js
export default function root(state = {}, action) {
switch (action.type) {
case ADD_PROMISE:
return {
...state,
promises: [...state.promises, action.promise]
};
case REMOVE_PROMISE:
return {
...state,
promises: state.promises.filter(p => p !== action.promise)
};
...
Теперь усовершенствуем действие fetchCard
:
// frontend/src/redux/actions.js
function fetchCard() {
return (dispatch, getState) => {
dispatch(startFetchingCard());
let url = apiPath() + "/card/" + getState().page.cardSlug;
let promise = fetch(url)
.then(response => response.json())
.then(json => {
dispatch(finishFetchingCard(json));
// "Я закончил, можете рендерить"
dispatch(removePromise(promise));
});
// "Я запустил промис, дождитесь его"
return dispatch(addPromise(promise));
};
}
Осталось добавить в initialState
пустой массив промисов и заставить сервер дождаться их всех! Функция render становится асинхронной и принимает такой вид:
// frontend/src/server.js
function hasPromises(state) {
return state.promises.length > 0
}
export default async function render(initialState, url) {
const store = configureStore(initialState);
store.dispatch(navigate(url));
let app = (
<Provider store={store}>
<App/>
</Provider>
);
// Вызов renderToString запускает жизненный цикл компонент
// (пусть и ограниченный). CardPage запускает фетч и так далее.
renderToString(app);
// Ждём, пока промисы закончатся! Если мы захотим когда-нибудь
// делать регулярные запросы (логировать пользовательское
// поведение, например), соответствующие промисы не надо
// добавлять в этот список.
let preloadedState = store.getState();
while (hasPromises(preloadedState)) {
await preloadedState.promises[0];
preloadedState = store.getState()
}
// Финальный renderToString. Теперь уже ради HTML.
let content = renderToString(app);
return {content, preloadedState};
};
Ввиду обретённой render
асинхронности обработчик запроса тоже слегка усложняется:
// frontend/index.js
app.get("*", (req, res) => {
const initialState = {
page: {
type: "home"
},
promises: []
};
render(initialState, {pathname: req.url}).then(result => {
const {content, preloadedState} = result;
const response = template("Habr demo app", preloadedState, content);
res.send(response);
}, (reason) => {
console.log(reason);
res.status(500).send("Server side rendering failed!");
});
});
Et voilà!
Заключение
Как вы видите, сделать высокотехнологичное приложение не так уж и просто. Но не так уж и сложно! Итоговое приложение лежит в репозитории на Github и, теоретически, вам достаточно одного только Docker, чтобы запустить его.
Если статья окажется востребованной, репозиторий этот даже не будет заброшен! Мы сможем на нём же рассмотреть что-нибудь из других знаний, обязательно нужных:
- логирование, мониторинг, нагрузочное тестирование.
- тестирование, CI, CD.
- более крутые фичи вроде авторизации или полнотекстового поиска.
- настройка и развёртка продакшн-окружения.
Спасибо за внимание!
#статьи
- 25 апр 2023
-
0
Экономим время с помощью искусственного интеллекта — повышаем свою квалификацию, эффективность и стоимость на рынке труда.
Иллюстрация: GPT-4 / Open AI / Simone Hutsch / Unspalsh / Annie для Skillbox Media
Любитель научной фантастики и технологического прогресса. Хорошо сочетает в себе заумного технаря и утончённого гуманитария. Пишет про IT и радуется этому.
Занимается NLP в стартапе Ex-Human.
Нейросети становятся всё более крутыми и мощными, а значит, игнорировать их — всё равно что стать луддитом и выступать против внедрения станков в производство. Конечно, можно провозгласить нейронки изобретением сатаны и таким образом отмахнуться от них. Однако факт остаётся фактом: кто не использует их в работе, теряет карьерные возможности.
Поэтому мы решили составить список из семи лучших «умных» нейросетей, которые помогут разработчикам писать код быстрее — а иногда даже с лучшим качеством.
Что умеет: писать код по текстовому описанию на всех популярных языках программирования, переводить код с одного языка программирования на другой, предлагать автодополнение больших фрагментов кода: генерировать готовые методы и тому подобное.
Источник: Copilot
Copilot — это одна из первых нейросетей, которая позарилась на работу программистов. Она использует модель OpenAI Codex, обученную на миллиарде строк кода, чтобы с нуля создавать целые функции и даже готовые алгоритмы в режиме реального времени.
У нейросети есть плагины для популярных сред разработки: Visual Studio Code, Visual Studio, Neovim и IDE от JetBrains. Выглядеть её работа может так: мы написали имя класса, а Copilot предложил для него готовый метод.
Скриншот: Copilot / Skillbox Media
Единственная проблема — стоимость подписки 10 долларов в месяц. Однако взамен вы получите удобный инструмент для ускорения работы с кодом.
Какие задачи можно решать: практически все, которые связаны с программированием.
Вот несколько примеров того, что Copilot умеет делать:
- дополнение кода — помогает разработчикам дописывать блоки кода за них, учитывая контекст программы;
- генерация кода — может генерировать фрагменты кода или создавать целые функции по текстовым описаниям;
- рефакторинг — предлагает варианты, как улучшить структуру кода;
- оптимизация кода — знает, как, например, заменить циклы на встроенные функции;
- исправление багов — умеет проводить диагностику и предлагать способы исправления ошибок.
Ещё Copilot можно использовать при изучении новых языков программирования. Делается это просто: выбираете язык и просите нейросеть показать пример кода, а затем пишете его самостоятельно, чтобы закрепить знания.
«Copilot может генерировать большие участки кода по текстовому описанию. Я использовала его, когда нужно было, например, написать код для соединения разных сервисов с СУБД (MongoDB и Redis). До этого мне не приходилось работать с ними, поэтому нейросеть писала всё сама. И хотя функции нужно было написать довольно базовые, радует то, что не пришлось долго изучать документацию и тратить на это время».
Арина Пучкова,
дата-сайентист, автор телеграм-канала «я обучала одну модель»
Как начать пользоваться: перейти на официальный сайт и оформить пробный период, чтобы протестировать все возможности нейросети.
Что умеет: дописывает код за разработчика, обучаясь на его старом коде.
Источник: Tabnine
Tabnine — альтернатива Copilot. Эта сеть умеет подстраиваться под стиль и частые задачи конкретного программиста, чтобы в дальнейшем писать код, похожий на то, что человек написал бы сам. Для этого нейросеть постоянно анализирует, как вы объявляете переменные, описываете методы и тому подобное.
Изображение: Tabnine
Ещё нейросеть способна упростить жизнь разработчикам, которые применяют несколько языков программирования: она помогает быстро переключаться между ними и предлагает рекомендации для каждого из них.
Tabnine можно встроить в популярные среды разработки. Вот список поддерживаемых IDE и редакторов кода с официального сайта проекта:
Источник: Tabnine
Какие задачи можно решать: те же, что и при помощи Copilot. Например:
- дополнять код и учитывать стиль разработчика;
- генерировать код — создавать фрагменты кода и целые функции по текстовому описанию;
- исправлять синтаксис, чтобы не приходилось тратить время на поиск ошибок;
- рефакторить код — делать проект более аккуратным и структурированным;
- оптимизировать код — убирать лишние переменные и улучшать алгоритмы.
И конечно, Tabnine тоже можно использовать как инструмент для изучения новых языков программирования и фреймворков.
Изображение: Tabnine
«В основном я использую Tabnine для дополнения кода. Обычно нейросеть даёт очень хорошие советы — например, предлагает имена недавно объявленных переменных, чтобы не прописывать их руками, а также показывает, какие операции можно с ними совершить.
Бесплатная версия не может сама написать большой кусок кода и иногда совершает ошибки. Поэтому бесплатно нейросеть можно использовать просто как инструмент, который автоматизирует несложные операции».
Арина Пучкова,
дата-сайентист, автор телеграм-канала «я обучала одну модель»
Как начать пользоваться: установить нейросеть себе прямо в IDE по инструкции на официальном сайте.
Что умеет: делает за программиста практически всё — например, пишет приложения с нуля, находит информацию в интернете и объясняет сложные вещи простыми словами.
Изображение: ChatGPT
ChatGPT — это один из самых полезных инструментов для программистов. Нейросеть представляет собой чат-бота на основе ИИ. Она умеет писать код и объяснять, как работают его отдельные части. А ещё ChatGPT помогает находить ошибки в коде и, например, генерировать документацию.
Возможности ChatGPT ограничиваются только вашим воображением. Вы можете попросить её написать нейросеть, которая будет отличать кошек от собак. А можете попросить объяснить вам, как работает та или иная функция в Python. И всё это на русском языке!
Мы, например, попросили ChatGPT рассказать простым языком, как работают нейросети:
Изображение: ChatGPT
Одна из важных особенностей ChatGPT — что она запоминает всё, о чём вы общались. И дальнейшие ответы генерирует, учитывая весь ваш диалог и его контекст.
Какие задачи можно решать: от написания простых функций до решения задач по программированию. ChatGPT может сделать практически всё, чем занимается джун.
«Я много использовала ChatGPT для задач в data science — например, размечала с помощью неё данные. Обращалась к ней, когда нужно было разобраться со сложными алгоритмами, и иногда просила объяснить, как работает какая-нибудь функция из неизвестного для меня фреймворка. А ещё просила переписать скрипт с C++ на Python».
Арина Пучкова,
дата-сайентист, автор телеграм-канала «я обучала одну модель»
Как начать пользоваться: прочитать нашу статью, зарегистрировать аккаунт в OpenAI и получить доступ к чат-боту.
Что умеет: генерировать команды терминала по текстовому описанию.
Изображение: Fig
Fig — это инструмент, который помогает программистам ускорить процесс написания кода. Работает он следующим образом: когда вы начинаете печатать код, Fig анализирует уже написанный код и предлагает подходящие варианты завершения строки.
Нейросеть обучили на миллионах строк кода, поэтому она знает огромное количество шаблонов функций и методов. Это полезно для новичков, которые ещё не знают всех доступных функций в языках программирования и сложившихся паттернов разработки.
Изображение: Fig
Встроить нейросеть можно в терминалы Bash, Zsh и Fish. Кроме того, Fig может работать с некоторыми популярными языками программирования — например, Python, JavaScript, TypeScript, PHP и Ruby.
Какие задачи можно решать: сокращать число команд, которые вы вводите вручную.
Изображение: Fig
Ещё Fig снижает количество ошибок в коде, выдавая рекомендации на основе уже существующего кода. Это особенно полезно, когда вы работаете с большими проектами, где простая ошибка может привести к критическим проблемам.
«Fig сокращает число операций, которые совершает программист. Инструмент предоставляет удобный интерфейс, в котором можно выбирать нужные пути до файлов, быстро дописывать команды в терминале и в целом ускорять свою работу».
Арина Пучкова,
дата-сайентист, автор телеграм-канала «я обучала одну модель»
Как начать пользоваться: скачать бесплатную версию с официального сайта и интегрировать нейросеть в свой терминал.
Что умеет: писать документацию к коду.
Изображение: Documatic
Documatic — это инструмент, который по текстовым запросам пользователей генерирует документацию к коду. Ещё он умеет строить зависимости и отвечать на любые вопросы по вашему коду, например о том, как работают определённые функции.
Чтобы создавать документацию, нейросеть анализирует комментарии к коду и его структуру. А если необходимо, она может сама описать, как код работает.
Какие задачи можно решать: автоматически генерировать описания функций, классов, методов и всего прочего.
Ещё Documatic может улучшить качество уже существующей документации, поскольку он обучен на большой кодовой базе и способен обеспечить более точные и последовательные описания, которые могут оказаться полезными для других разработчиков.
Как начать пользоваться: перейти на официальный сайт нейросети, залогиниться и следовать инструкциям.
Что умеет: писать документацию для кода.
Изображение: Mintlify
Mintlify — это ещё один инструмент для автоматической генерации документации. Он очень простой и при этом поддерживает 12 языков программирования — например, Python, JavaScript и PHP.
Чтобы создать описание функции, нужно всего лишь выделить её и кликнуть на кнопку Generate Docs. Дальше нейросеть напишет, что это за функция, какие параметры она принимает и учтёт другие необходимые нюансы.
Единственное ограничение Mintlify — она доступна только в VS Code и IntelliJ IDEA. Для других IDE плагинов пока нет.
Какие задачи можно решать: быстро генерировать документацию для любых фрагментов кода. Или, как говорят создатели Mintlify: «Писать документацию — отстой. Позвольте Mintlify сделать это за вас. Просто выделите код и узрите магию».
Как начать пользоваться: перейти на официальный сайт и выбрать подходящую версию инструмента. После этого установите плагин и пользуйтесь им.
Что умеет: анализировать код и искать в нём уязвимости.
Скриншот: Snyk Code / Skillbox Media
Snyk Code — это нейросеть для быстрого анализа кода на уязвимости. Она может проверять не только написанный вами код, но и обнаруживать проблемы в безопасности в сторонних библиотеках и фреймворках. Это может быть особенно полезно для больших проектов, где используется много внешних библиотек.
Ещё Snyk Code можно применять в командной разработке, чтобы проверять код коллег на уязвимости и вместе быстрее исправлять их.
Какие задачи можно решать: быстро обнаруживать уязвимости и исправлять их до того, как они попадут в продакшен. Ещё нейросеть Snyk Code можно интегрировать в процесс разработки и использовать его в качестве постоянного инструмента для тестирования.
Бесплатная версия Snyk Code позволяет проводить до 200 проверок в месяц. А платная версия снимает это ограничение и добавляет интеграцию с Jira.
Как начать пользоваться: перейти на официальный сайт, залогиниться в свой аккаунт и интегрировать его в свой GitHub-аккаунт.
Мы хотим донести одну простую мысль — не надо бороться с ИИ, надо уже сейчас учиться встраивать его в свои процессы разработки, учиться вводить качественные запросы, подстраивать и обучать под свои задачи. Это уже не будущее, а настоящее.
Спасибо Арине Пучковой за подборку полезных нейросетей и помощь в написании статьи. Обязательно подписывайтесь на её телеграм-канал, чтобы узнать больше о мире data science на практике.
Научитесь: Профессия Data Scientist
Узнать больше
Разбираемся, что такое конструкторы мобильных приложений, как они устроены и в каких сервисах можно собирать программы для телефонов самостоятельно.
Конструктор для создания приложений — это готовое решение, в котором пользователь может собрать свою программу из готовых шаблонов, блоков и элементов.
Выделяют несколько типов конструкторов:
-
Ноукод-конструкторы. Предлагают широкий набор функций по созданию мобильных приложений без знаний в программировании. Но при этом потребуется разбираться в работе с базами данных и скриптах.
-
Шаблонные конструкторы. Код приложения закрыт — его нельзя менять или настраивать под себя. Приложения получаются похожими друг на друга, набор функций сильно ограничен.
Готовые приложения, которые разрабатывают в конструкторах, делят на две категории:
-
Нативные. Готовые установочные файлы для магазинов приложений. Это полнофункциональные приложения, которые могут работать без подключения к интернету.
-
PWA. Веб-приложение, которое работает только в окне браузера. Часто недоступно для пользователей без подключения к интернету.
Читайте также:
Что такое гибрид сайта и мобильного приложения PWA и SPA?
Важная деталь: AppStore может не пустить в магазин приложений шаблонную программу. Поэтому для успешного релиза лучше использовать ноу-код конструкторы и стараться не брать готовые шаблоны.
Draft Bit
Тип: ноукод-конструктор.
Доступные приложения: нативные и PWA.
Блочный визуальный конструктор для мобильных приложений с широкими возможностями. Draft Bit хорош тем, что позволяет создавать приложения, рисуя и размещая элементы на окнах приложения, — это похоже на то, что делают в Figma, когда создают макеты. Такую систему еще называют drag&drop, но плюсы Draft Bit в том, что все это работает с помощью реального кода. Его можно выгрузить и доработать в другой программной среде.
Интерфейс редактора Draft Bit
Возможности конструктора приложений Draft Bit:
-
Множество шаблонов, каждый можно настроить.
-
Три уровня элементов для работы: биты для простых компонентов (например, кнопок), модули для построения систем и доступ к прямому кодингу.
-
Настройка дизайна элементов: шрифты, иконки, темы.
-
Конструктор меню и навигации в приложении.
-
Доступ к любому API для выгрузки данных.
-
Интеграция с внешними сервисами.
-
Открытый код, который можно посмотреть во время создания приложения.
-
Поддержка командной работы.
Пример приложения для вызова клинига на дом, разработанный в Draft Bit
Плюсы:
-
Есть приложение и отдельная программа для использования на компьютере.
-
Прямая публикация в магазинах приложений.
-
Не нужно платить за лицензию после публикации приложения, оплата — только во время работы в сервисе.
-
Экспорт исходного кода для дальнейшей доработки в других программных средах.
Минусы:
-
Конструктор больше заточен под интерфейсы, а не системы.
-
Возможна проблема с оплатой подписки при помощи российских карт.
Тарифные планы конструктора приложений Draft Bit
Стоимость начинается от 59 долларов в месяц при единовременной оплате подписки на год. В этот начальный тариф входит полный доступ к сервису и возможность создать два приложения. Повышенные тарифы увеличивают количество возможных приложений — от 129 до 999 долларов в месяц при оплате подписки на год. Есть триал на 14 дней, чтобы изучить функционал.
Kodika
Тип: ноукод-конструктор.
Доступные приложения: нативные.
Конструктор приложений для macOS и iPhone, работающий по принципу перетаскивания и настройки элементов на дашборды. Сервис Kodika позволят выстроить логику и структуру будущего приложения без знания кода — для этого есть специальный раздел «бэкэнда», где вы можете связать элементы между собой и прописать простые скрипты.
Интерфейс конструктора приложений Kodika
Возможности конструктора приложений Kodika:
-
Сотни готовых блоков, шаблонов, кнопок, иконок и списков.
-
Адаптивный дизайн: каждый элемент можно настроить.
-
Интерактивные элементы.
-
Конструктор меню и структуры — можно реализовать свою логику приложений по готовым модулям.
-
Поддержка swift-плагинов с открытым/закрытым кодом — это язык программирования от Apple, который позволяет делать приложения полнофункциональными.
-
Поддержка REST API и интеграций.
Читайте также:
Мобильная версия сайта vs адаптивная верстка: отличия, плюсы и минусы
Плюсы:
-
Есть бесплатная версия.
-
Прост в освоении.
Минусы:
-
Нет аналитики при релизе в магазинах приложений.
-
Конструктор заточен под интерфейсы, а не системы.
-
Сервис не работает на Windows и не создает андроид-приложения, потому что работает на коде Apple.
-
Возможна проблема с оплатой подписки при помощи российской карты.
Тарифы конструктора приложений Kodika
Базовая версия конструктора доступна бесплатно — в ней ограничен функционал, а за релиз приложения придется заплатить отдельно — 79,99 евро. Стандартная подписка стоит от 24,99 евро в месяц и включает в себя неограниченные возможности по разработке приложения.
Adalo
Тип: ноукод-конструктор.
Доступные приложения: нативные и PWA.
Конструктор приложений, который работает в формате слайдов — вы «рисуете» экраны приложения, а потом связываете между собой кнопки и прописываете логику. Все элементы (блоки, модули) можно перемещать, менять их размеры и дизайн. Конструктор поддерживает базы данных — это позволит создавать полнофункциональные приложения.
Интерфейс конструктора приложений Adalo
Возможности конструктора приложений Adalo:
-
Работа на трех уровнях: с дизайном компонентов, прописыванием скриптов-действий и базой данных в таблицах.
-
Разнообразные компоненты: блоки, графики, кнопки, списки, платежи, формы, навигация.
-
Можно взаимодействовать с устройством пользователей для использования камеры или телефонной книги.
-
Отправка пуш-уведомлений.
-
Продвинутая работа с базой данных: формулы, коллекции, роли, данные пользователей.
-
Создание нативных приложений с последующей публикациях в магазинах App Store и Google Play.
-
Поддерживает интеграции с внешними сервисами и работу через API.
Пример приложения Chant, созданного на Adalo
Плюсы:
-
Есть бесплатная версия.
-
Простая разработка по принципу drag&drop.
-
Можно создавать приложения для веба и телефонов.
Минусы:
-
Ограниченное число элементов для работы и доступных действий по построению логики.
-
Могут быть проблемы с оплатой и доступом к конструктору из России.
Тарифы Adalo
В бесплатной версии можно создавать неограниченное число приложений, но публиковаться они будут только в вебе на домене конструктора. Снять ограничение помогут платные тарифные планы — начиная от 50 долларов в месяц можно будет релизить приложения в магазинах и работать с полным функционалом: прописывать логику приложения, подключать базы данных, интегрироваться с другими сервисами.
Shoutem
Тип: шаблонный конструктор.
Доступные приложения: нативные и PWA.
Конструктор заточен в первую очередь на работу с малым и средним бизнесом — здесь уже продуманы модули для каталогов, онлайн-покупок, программ лояльности, событий. В Shoutem стильные и современные дизайн-шаблоны, но при этом они сильно ограничены и не подходят для построения своих систем.
Читайте также:
Мобильные приложения для малого бизнеса: 9 главных вопросов
Интерфейс конструктора приложений Shoutem
Возможности конструктора приложений:
-
Сервис предлагает готовые решения и шаблоны для магазинов, радиостанций, образовательных учреждений, концертных площадок, туризма.
-
Потоковая передача медиа, интеграция с YouTube, картами и веб-сайтами.
-
Можно подключить рекламные кабинеты для заработка на аудитории.
-
Встроенная аналитика по действиям пользователей приложения.
-
Публикация в Google Play и App Store.
-
Работа с базами данных через расширения.
-
Пуш-уведомления.
Примеры приложений для туризма и шоппинга, созданных в Shoutem
Плюсы:
-
Стильные шаблоны.
-
Множество элементов для настройки дизайна.
-
Богатый набор функций.
-
Поддержка интеграции и расширений.
-
Есть шаблоны для создания соцсетей и стриминговых площадок.
Минусы:
-
Ограниченный конструктор.
-
Мало возможностей для индивидуализации шаблонов.
-
Возможна проблема с оплатой конструкта из России.
Тарифы Shoutem
Стоимость начинается от 59 долларов в месяц за базовую версию с возможность создавать андроид-приложения. Повышение тарифа до 99 долларов открывает доступ к iOS-приложениями и расширяет возможности конструктора. Более высокий тариф за 189 долларов в месяц открывает шаблоны для разработки соцсетей. На всех тарифах есть бесплатный двухнедельный триал.
App Maker
Тип: шаблонный конструктор.
Доступные приложения: нативные.
Простой конструктор приложений, который позволяет сделать шаблонное приложение за несколько шагов. App Maker не умеет работать с логикой и скриптами — вариантов для разработки уникальных приложений мало.
Интерфейс App Builder
Возможности конструктора приложений App Maker:
-
Готовые шаблоны для приложений.
-
Редактирование уже выпущенного приложения с последующим обновлением у пользователей.
Плюсы:
-
Встроенная публикация в AppStore и Google Play с доступом к аналитике.
-
Автономная работа приложений.
Минусы:
-
Нет бесплатной версии.
-
Нет инструмента для работы с логикой и системами.
-
Приложения получаются шаблонными.
-
Устаревший дизайн.
Читайте также:
Основные признаки устаревшего сайта
Тарифы в App Builder
Стоимость — от 999 рублей в месяц за приложение в Google Play с ограниченным числом скачиваний. Расширенный тариф для публикации в AppStore — от 1 999 до 2 999 рублей в месяц.
Glide
Тип: ноукод-конструктор.
Доступные приложения: PWA.
Сервис для создания приложений через через гугл-таблицы. На Glide можно создавать CRM-системы, делать онлайн-каталоги, сервисы по подбору товаров и контент-площадки. Работают готовые приложения по технологии PWA — они доступны как на телефонах, так и на компьютерах, но только в формате страницы в браузере.
Интерфейс Glide
Возможности конструктора приложений Glide:
-
Более 40 компонентов-блоков для построения приложений и 90+ вариантов столбцов.
-
Возможность встраивать веб-код в свой проект.
-
Электронная коммерция, кнопка «Купить» и штрих-сканер.
-
Командная работа.
-
Программирование сложных приложений без знания кода.
Пример приложения, которое собрано с помощью Glide, — оно создано на бесплатном тарифе, поэтому карты работают с ограничениями
Плюсы:
-
Относительно прост в освоении.
-
Есть шаблоны.
-
Скрипты прописываются через гугл-таблицы.
Минусы:
-
Нельзя выложить отдельным приложением.
-
Работает только как сайт на домене.
-
Не запустится или будет плохо работать без интернета.
-
Комиссия от 2 % до 10 % за все платежи внутри приложения.
-
Потребуется знания по работе с базами данных и навыки в гугл-таблицах.
-
Ограниченное число интеграций с другими сервисами, русскоязычные — не поддерживает.
Тарифы Glide
В бесплатной версии доступна только мобильная версия, а количество обновлений гугл-таблиц ограничено — при достижение лимита у пользователей перестанет работать сервис. Начиная от 25 долларов в месяц функционал расширяется — можно прикреплять свой домен и подключить неограниченное число обновлений.
Appsfera
Тип: шаблонный конструктор.
Доступные приложения: нативные.
Конструктор мобильный приложений Appsfera позиционируется авторами как решение для малого и среднего бизнеса. По словам разработчиков, в нем можно создать минимально жизнеспособный продукт (MVP): когда приложение работает как веб-сервис на телефоне. Но скорее это программа для разработки шаблонных приложений с минимальным набором функций.
Читайте также:
Быстрый запуск интернет-магазина: MVP и то, что можно отложить «на потом»
Интерфейс Appsfera
Возможности конструктора приложений Appsfera:
-
Разработка приложений по готовым шаблонам.
-
Пуш-уведомления.
-
Возможность создавать маркетплейсы, каталоги и магазины.
-
Дополнительные функции: онлайн-чат, онлайн-запись, меню, навигация.
Пример приложения, созданного в Appsfera: сервис для записи в барбершоп «Кузьма»
Компания-разработки сама занимается созданием приложений — они делали сервисы для «ВТБ», «Бургер Кинг» и других крупных компаний. Их конструктор — это скорее набор элементов, из которых можно собрать более-менее рабочее приложение в качестве временной заглушки. Поэтому действующих приложений не так много, хоть в портфолио на сайте и представлены примеры, найти их в Google Play не удалось.
Плюсы:
-
Встроенная аналитика.
-
Есть бесплатная версия.
-
Интеграция с другими сервисами: «Ю-Kassa», «Яндекс.Карты», YClients и т. д.
-
Элементы конструктора заточены под малый бизнес.
-
Есть видеоуроки, которые помогут разобраться с функциями.
-
Есть свой маркетплейс с шаблонами или готовыми приложениями.
Минусы:
-
Ограниченный выбор в настройке дизайна приложений.
-
Не умеете работать с логикой и скриптами.
-
Устаревший дизайн и решения.
Тарифы Appsfera
На бесплатном доступно 42 модуля, не работает онлайн-оплата и показывается реклама. Публикация в магазинах приложений — самостоятельная, тариф действует при условии 100 скачиваний. Далее на тарифах «старт», «базовый» и «премиум» стоимость вырастает с 790 до 4 790 рублей в месяц, а набор функций расширяется.
Ещё 11 конструкторов мобильных приложений
Рассмотрите еще варианты конструкторов мобильных приложений, которые могут пригодится в 2022 году:
-
AppsGeyser. Простой и бесплатный шаблонный конструктор для быстрого постинга приложений в Google Play. Доход от встроенной рекламы делят с автором приложения.
-
Apps Global. Модульный конструктор приложений. Есть триал, подписка — от 7,99 долларов в месяц.
-
Bubble. Продвинутый конструктор приложений для зеркодинга в PWA. Есть бесплатная версия.
-
Mo Apps. Простой конструктор приложений от российских разработчиков с минимальным набором шаблонов. От 50 долларов в месяц.
-
Mobium. Российский сервис для разработки мобильных приложений для интернет-магазинов, который адаптирует готовый каталог. Стартовая цена — от 12 тысяч рублей.
-
Goodbarber. Шаблонный конструктор с большим набором функций. От 25 евро в месяц.
-
BuildFire. Конструктор для разработки приложений под iOS и Android. От 159 долларов в месяц.
-
NWcode. Российский ноукод-конструктор с возможность сделать свой сервис или облако для приложения.
-
SmartApp Graph. Онлайн-конструктор для создания ассистентов в «Сбер Салют».
-
Bravo Studio. Конвертирует макеты из Figma в приложения.
-
Andromo. Шаблонный конструктор для разработки мобильных приложений. От 8 долларов в месяц.
Читайте также:
Самые популярные CMS в Рунете
В заключение
Возможности любого конструктора мобильных приложений ограничены — вы работаете с чужим кодом и решением, поэтому приходится опираться на логику сервиса. Часто для поддержки требуется оплачивать подписку или покупать лицензии — разработанное приложение вам не принадлежит полностью, а скорее предоставляется как услуга. Если требуется протестировать гипотезу, создать MVP или выпустить временное приложение — конструкторы использовать можно.
Читайте также:
Что такое продуктовый подход к разработке мобильных приложений и почему он реально работает
Но для разработки полноценных магазинов, соцсетей, служб доставки, такси лучше разрабатывать собственные нативные программы. Найти исполнителей поможет тендерная площадка Workspace — через нее ищут исполнителей для разработки служб каршеринга, интернет-магазинов, логистических решений.
Тендеры на разработку мобильных приложений в Workspace
Размещение любого тендера — бесплатно, вам остается только поставить цену и описать название. Сервисом пользуется более 17 тысяч агентств и фрилансеров — собирайте их отклики и создавайте с помощью них полнофункциональные мобильные приложения. Также посмотрите раздел «Кейсы», где компании делятся результатами своей работы по созданию программ для телефонов.
Но не все могут позволить себе потратить кругленькую сумму (в пять или шесть знаков) на программистов, потому что на начальном этапе бюджет, как правило, ограничен.
К счастью, на рынке появилось множество сервисов, которые помогут начинающим предпринимателям создать веб-сайт или приложение без единой строки кода. Определенно, они сэкономят время и деньги. На PrimeLiber опубликовали подборку из 7 инструментов, популярных среди пользователей Product Hunt.
1. Bubble
https://bubble.is/
В основу сервиса заложена концепция визуального программирования, то есть программирования без кода. Технология «drag & drop» позволяет легко добавлять и перемещать элементы страницы: текст, видео, карты, иконки, изображения, кнопки и пр. Все поддается настройке вплоть до цвета фона, иконок, прозрачности элементов.
Workflow-программирование дает более детальное представление о том, что происходит на каждом шагу. В Bubble можно структурировать и хранить данные, задавать свою логику переходов (например, если пользователь при входе в систему нажал кнопку X, перейти к Y, в противном случае – к A), кроме того у ваших пользователей появляется возможность загружать свой контент.
Здесь реализована интеграция с такими популярными сервисами, как MailChimp (автоматическая подписка пользователей) и Mixpanel (отслеживает активность пользователей в приложении). Это идеальный вариант для тех, кто хочет создать сайт или приложение, особо не заморачиваясь. Создать проект в сервисе можно бесплатно. Плата взимается после того, как как количество пользователей начнет расти, и сайтом или приложением начинают активно пользоваться.
Тара Рид, основатель Kollecto
У меня нет технического образования, и в своих разработках я использую Bubble. С его помощью мне удалось сделать MVP проекта, он начал приносить доход, стал резидентом акселератора 500 Startups. Почитать про опыт создания Kollecto в Bubble можно здесь – http://bit.ly/1WISExa Для тех, у кого нет навыков программирования и лишних средств на разработчиков, Bubble станет идеальным решением. Да, я могла бы нанять разработчиков, но опять же, как понять, что они хорошо справились с поставленной задачей. А тратить деньги на слабеньких программистов не хотелось. Создатели сервиса славно поработали. Поздравляю, ребята, это успех.
2. Pixate
http://www.pixate.com/
Pixate генерирует 100% родные прототипы, так что вы сразу сможете «пощупать» ваши идеи на устройстве. Всего несколько кликов, и вы добавляете необходимую анимацию и интерактивные жесты (и все это без единой строчки кода).
Сервис очень популярен среди дизайнеров, которым необходимо быстро создать качественный анимационный прототип для презентации заказчику. Pixate полностью переворачивает представление о способах прототипирования и lean-тестировании. Крайне рекомендуем всем, кто в ближайшее время собирается создать или оптимизировать мобильное приложение.
Pixate Studio можно скачать бесплатно. Pixate Cloud стоит $5 в месяц на одного пользователя или $50 в год. Есть бесплатная trial-версия на 30 дней.
Дамиан, графический дизайнер Red-Sky
Пользовался сервисом пару недель назад и должен признать, что это самый лучший инструмент для создания прототипов. Он мегапростой и супербыстрый.
3. Treeline
https://treeline.io/
Сейчас доступна только бета-версия сервиса, но мы уверены, что со временем он станет мегапопулярным. Treeline позволяет создавать корпоративные серверные приложения за считанные часы (хотя раньше на это уходили месяцы). В нем вы задаете архитектуру бэкенда в виде небольших, многократно используемых, хорошо отлаженных модулей. Вот неполный список возможностей, доступных в Treeline:
- отправка HTTP-запросов
- сравнение паролей и кодировка
- работа со строками в JavaScript
- доступ к API ElasticSearch через Node.js
- доступ к API YouTube для загрузки видео
- доступ к API Stripe для приема онлайн-платежей
- интеграция с Facebook для аутентификации пользователей, получения персональных данных
Мика Болдвин, создатель проектов
Создатели очень умны, начали с Sails.js (похож чем-то на Rails) и сделали мощный продукт, который позволяет разработчику создавать backend-код … без написания самого кода. А вот интересно, когда совсем отпадет необходимость писать код, и все будет конструироваться с помощью drag&drop интерфейсов?
Tilda Publishing
http://tilda.cc/ru/
Tilda позволяет создавать красочные контентные страницы. Вы собираете страницу из готовых блоков (их больше 170), которые хранятся в библиотеке системы. Все страницы, сделанные на Tilda – адаптивные, вам не придется беспокоиться о том, как они будут выглядеть на разных девайсах. Несомненный плюс для непрограммистов.
Веб-страницы получаются яркими и эффективными с хорошей типографикой и визуальными эффектами. Сервис позволяет применять различные функциональные элементы для призыва к действию (отзывы, кнопки, формы, списки преимуществ), подключаться к Google Analytics, оптимизировать страницу под поисковики.
Сервис весьма полезен для создания блогов, портфолио, новостных и корпоративных сайтов.
Бесплатно можно создать только один сайт из 5 страниц. За использование сервиса в «промышленных масштабах» придется платить.
Фред Риветт, член команды @wecontrast
Увидел, как Никита пару недель назад запустил продукт, и сразу решил попробовать. Конечно, в приоритете здесь нестандартные решения UI-дизайна, и выглядят так, словно вы отвалили за них кучу денег агентству. Передо мной встала дилемма. Мне нравиться работать над дизайном и кодить. Инструмент доступен по цене, прост в использовании, и прекрасен. Теперь хорошенько подумаю, стоит ли тратить время и писать все с нуля, или за небольшие деньги получить тот же результат. Прошло то время, когда компании заказывают сайты у агентств. А зачем, если появились такие сервисы, как Tilda? Недостатки? Ваш сайт не будет претендовать на уникальность, но сейчас большинство сайтов выполнены в едином стиле (и вообще-то смотрятся очень неплохо). Так что для 99,9% сайтов это вовсе не является минусом.
Webflow CMS
https://webflow.com/
CMS система предназначена для сайтов с динамичным контентом, созданных в Webflow. В сервисе вы задаете вид контента (блог, портфолио) и далее адаптируете под него структуру сайта. Если у вас нет четкого понимания, как должен выглядеть сайт, используйте уже существующие шаблоны.
Сервис позволяет легко и быстро создавать большое количество однотипных страниц. К примеру, вы можете создать свой шаблон «страницы блога», и он будет автоматически применятся к другим страницам с таким же ярлыком. Вы можете вносить правки и редактировать сайт в режиме реального времени – контент будет обновляться автоматически. У Webflow удобный интерфейс и простая навигация.
Бесплатно можно работать только над одним проектом, при этом за хостинг придется платить.
Леонардо Закур, предприниматель и дизайнер
Я большой поклонник Webflow! Прекрасная команда и прекрасный продукт. Приложение великолепно сделано, действительно быстрое и отзывчивое (это, кстати, веб-приложение). Webflow занимает в нашем рабочем процессе очень важное место. Он позволяет одновременно создавать и дизайн, и фронтенд. А UI/UX дизайнеры постоянно контролируют то, как будет выглядеть и ощущаться готовый продукт. Превосходно, что можно быстро проверить, как будет выглядеть и работать сайт, причем в любом браузере и на любом устройстве. Посмотреть шрифты, анимацию, переходы. И все это на стадии проектирования. В нашей студии Bons мы используем Webflow на более профессиональном уровне, поскольку его можно донастроить.
Webflow 3D Transforms
http://3d-transforms.webflow.com/
Этот инструмент так же, как и предыдущий, создан командой Webflow, и считается лучшим в своем классе. С ним вы получаете расширенные возможности по созданию 3D и CSS трансформаций при анимации сайтов. Элементы страницы можно перемещать, вращать, применять к ним перспективу.
Вот только некоторые функции: создание анимации с эффектом переворачивания (card flip), многослойный 3D-эффект, изометрическая проекция.
Сейчас доступна бета-версия продукта.
Рэнди Эллис – ведущий преподаватель по UX-дизайну
Вау, они снова это сделали! Я с самого начала был их преданным клиентом, а теперь появилась еще одна причина получше присмотреться к сервису. Сначала Webflow, потом Webflow CMS, а теперь еще и 3D Transforms. Похоже ребята хотят, чтобы Webflow навсегда поселился в нашей экосистеме. И это здорово!
7. Cloudpress
http://cloud-press.net/
С помощью CloudPress можно создать уникальные, адаптивные WordPress-сайты. Готовые блоки (а их более 80) позволяют сконструировать прототипы страниц за считанные минуты. В ThemeBuilder можно задать ширину страницы или сделать ее адаптивной.
Вы получаете контроль над каждой деталью сайта. Можно менять размер элементов, типографику, фон, эффекты.
Минимальная плата $11,99 в месяц, создать можно не более 3 сайтов.
Нейт Хэнсон, один из основателей Sumry
Великолепный инструмент. Чем-то похож на Squarespace.
Надеемся, вы нашли для себя что-то полезное и в ближайшем будущем опробуете эти инструменты. Как видите, совершенно не обязательно быть гением программирования, чтобы создать красивый сайт или приложение. Ждем ваших шедевров.
Бонус:
Так же Вам может понравится еще одна подборка – 6 Сервисов от Product Hunt для вашего стартапа.
Содержание
- Создаем собственное программное обеспечение для Windows
- Способ 1: Программы для написания программ
- Способ 2: Язык программирования и среда разработки
- Вопросы и ответы
Ежедневно каждый активный пользователь компьютера сталкивается с работой в разных программах. Они призваны облегчить работу за ПК и выполняют определенный ряд функций. Например, калькулятор подсчитывает заданные примеры, в текстовом редакторе вы создаете документы любой сложности, а через плеер просматриваете любимые фильмы или слушаете музыку. Весь этот софт был создан с помощью языков программирования, начиная от основных элементов управления, и заканчивая графическим интерфейсом. Сегодня мы бы хотели обсудить два метода собственноручного написания простых приложений для операционной системы Windows.
Создаем собственное программное обеспечение для Windows
Сейчас разработать свою программу можно и без знания языков программирования, однако для этого существует совсем мало подходящих средств, позволяющих в полной мере реализовать задуманное. К тому же сейчас на просторах интернета бесплатно доступно множество курсов по ЯП, описывающих примеры написания софта с предоставлением исходного кода. Поэтому поставленная задача вполне реализуема, нужно лишь выбрать метод, что мы и предлагаем сделать далее.
Способ 1: Программы для написания программ
Если вы интересовались созданием игр, то знаете о специальных инструментах, куда уже встроено множество компонентов и записаны основные скрипты. Юзеру остается лишь создать из этого цельную картину, систематизируя имеющиеся данные. Примерно по такому же принципу работает и ПО, позволяющее создавать собственные приложения без знания языков программирования. За пример мы взяли HiAsm, поскольку это единственное оптимальное решение с полной локализацией на русский язык.
Скачать HiAsm Studio с официального сайта
- Сразу приступим к рассмотрению простой инструкции по созданию примитивной программы в HiAsm. Для начала перейдите по указанной выше ссылке, чтобы скачать и установить используемый сегодня инструмент.
- После запуска ознакомьтесь с представленной информацией по использованию и решению частых проблем. Сразу хотим отметить, что некоторые антивирусы распознают HiAsm как вредоносный код, блокируя запускающиеся файлы. Поэтому при возникновении неполадок рекомендуем добавить инструмент в исключения или на время выключать защиту операционной системы.
- Через меню «Файл» создайте новый проект.
- Появится новое окно с выбором различных типов приложений. Сегодня мы хотим сконцентрироваться на стандартной программе для Windows с графическим интерфейсом.
- За пример возьмем простое электронное меню с выбором блюд через всплывающий список, а также с возможностью указания количества необходимых порций. Данный выбор был сделан лишь для того, чтобы продемонстрировать работу основных элементов HiAsm. Сначала перейдем к добавлению нового элемента в главное окно, нажав на соответствующую кнопку.
- В открывшемся окне вы увидите, что все объекты распределены по группам, чтобы было удобно выбирать требуемое. Создадим всплывающий список, нажав по нему.
- Переместите элемент на рабочую область, а затем соедините с главным окном.
- Дважды щелкните по списку, чтобы заполнить строки. Каждую новую позицию пишите с новой строки.
- Подтвердите изменения, щелкнув на зеленую галочку.
- Теперь давайте добавим обычный текст, который будет свидетельствовать о названии всплывающего меню.
- Откройте объект и заполните его содержимым.
- Обозначим надпись дополнительной картинкой, выбрав соответствующий элемент из списка.
- Все это тоже нужно будет связать с главным окном.
- HiAsm поддерживает изображения разных размеров и форматов, добавляется оно точно так же, как в случае с текстом.
- Дополнительно присутствует встроенный редактор, позволяющий изменить определенные части картинки.
- Далее через «Вид» вы можете запустить «Редактор формы».
- Он позволит расположить все компоненты в необходимом месте на окне путем перемещения и масштабирования.
- Каждый объект или меню редактируется через окно «Свойства элемента». Запустите его, чтобы увидеть основные параметры, предварительно выбрав одно из меню или окон.
- Здесь вы можете менять основной фон, устанавливать размеры, расположение курсора, положение относительно основного окна и добавить одну из множества точек.
- Окно свойств по умолчанию находится справа. Давайте обратим внимание на редактирование текста. Выберите шрифт, цвет и размер. В разделе «Style» активируется курсив, подчеркивание или выделение жирным.
- Добавим перемещаемый ползунок, чтобы регулировать количество порций.
- В меню «Свойства» потребуется настроить минимальное и максимальное значение отметок, например, от 1 до 6.
- После каждого изменения можете запускать программу, чтобы ознакомиться с результатами и убедиться в отсутствии ошибок.
- По завершении мы предлагаем добавить кнопку «ОК», подтверждающую готовность заказа. Она находится в разделе «Rush-Контролы».
- Задайте кнопке название, например «ОК» или «Подтвердить заказ».
- После завершения добавления двух позиций у нас получилась программа, которую вы видите на скриншоте ниже. Конечно, здесь еще нужно работать с оформлением и другими недостатками функциональности, внешнего вида. Однако этот пример был создан только ради того, чтобы продемонстрировать принцип действия HiAsm.
- Если хотите сделать перерыв или сохранить готовый проект для дальнейшего конвертирования в исполняемый файл, нажмите на кнопку «Сохранить» и выберите место на жестком диске.
Возможностей рассмотренного инструмента хватит не только для того, чтобы создать простое графическое приложение. HiAsm вполне справляется и с гораздо сложными работами, например, созданием проигрывателя или загрузчика файлов из интернета. Конечно, здесь придется приложить намного больше усилий и выучить множество схем и встроенных скриптов. Все это намного проще освоить, если использовать официальные ресурсы, например, форум. Там пользователи не только делятся своими работами, но и объясняют начинающим азы конструирования ПО. Тем более при возникновении вопросов ничего не мешает вам создать отдельную тему, подробно описав сложившуюся трудность.
Перейти на официальный форум HiAsm
Способ 2: Язык программирования и среда разработки
Как уже было сказано ранее, абсолютно все программы пишутся на определенном языке программирования. В некоторых сложных проектах бывает задействовано сразу несколько ЯП. Такой способ написания софта самый сложный, но при освоении одного из языков вы получаете практически безграничные возможности в кодировании программного обеспечения, утилит или отдельных скриптов. Главная задача — определиться с языком программирования. На этот вопрос постарались дать ответ специалисты из известного обучающего сервиса GeekBrains. Всю необходимую информацию вы найдете по указанной ниже ссылке.
5 языков программирования, которые надо учить первыми
Теперь же давайте рассмотрим несколько вариантов обеспечения, написанного при помощи упомянутых в статье ЯП. В первую очередь затронем Python, который некоторые программисты считают самым простым языком. Чтобы на экране появилось простое графическое окно размером на весь экран, придется подключить стандартную библиотеку Tkinter и написать код такого формата:
from tkinter import *
class Paint(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
def main():
root = Tk()
root.geometry("1920x1080+300+300")
app = Paint(root)
root.mainloop()
if __name__ == "__main__":
main()
Далее добавляется код, который вы видите на скриншоте ниже. Он реализует примерно такие же функции, как стандартная программа Paint.
После успешной компиляции запускается графическое окно с уже добавленными кнопками. Каждая из них отвечает за размер кисти и цвет.
Как видите, разобраться в приложениях с GUI (графическим интерфейсом) не так уж и сложно, однако сначала лучше начать с консольных скриптов и небольших программ. Освоить Python вам помогут свободные материалы, уроки и литература, которой сейчас вполне достаточно, чтобы самостоятельно изучить необходимый материал.
В приведенной статье на GeekBrains отдельное внимание уделено и C#, который называют универсальным языком программирования для тех, кто еще не определился, в какой области хочет применять свои навыки. Разработка ПО для Windows ведется в официальной среде от Microsoft под названием Visual Studio. Код внешне выглядит так, как вы видите ниже:
namespace MyWinApp
{
using System;
using System.Windows.Forms;
public class MainForm : Form
{
// запускаем приложение
public static int Main(string[] args)
{
Application.Run(new MainForm());
return 0;
}
}
}
Как видите, существуют определенные различия с тем же Python. Если скопировать этот код, вставить его в IDE и скомпилировать, на экране появится простейшее графическое окно, куда уже в дальнейшем и будут прикрепляться кнопки, блоки и другие объекты.
Мы упомянули о Visual Studio как о среде разработки. Она понадобится в любом случае, если вы хотите писать собственный софт на ЯП, поскольку стандартный блокнот или текстовый редактор для этого практически не подходит. Ознакомиться с лучшими IDE, поддерживающими разные языки, мы советуем в отдельной нашей статье от другого автора далее.
Подробнее: Выбираем среду программирования
В рамках этой статьи мы постарались максимально детально ознакомить вас с процессом написания программного обеспечения с помощью двух доступных методов. Как видите, дело это не совсем простое, ведь нужно получать специальные знания и учить многие аспекты, чтобы освоиться в этом деле. Приведенный выше материал был нацелен лишь на предоставление общей информации для ознакомления и не является полноценным уроком, освоив который, можно стать уверенным программистом. Если вас заинтересовал какой-либо ЯП или HiAsm, потребуется уделить много времени на изучение соответствующих обучающих материалов.