Итак, вы решили сделать новый проект. И проект этот — веб-приложение. Сколько времени уйдёт на создание базового прототипа? Насколько это сложно? Что должен уже со старта уметь современный веб-сайт?
В этой статье мы попробуем набросать 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.
- более крутые фичи вроде авторизации или полнотекстового поиска.
- настройка и развёртка продакшн-окружения.
Спасибо за внимание!
- «Web user interface», «Web-based user interface», «WUI» and «Web UI» redirect here.
Horde groupware is an open-source web application.
A web application (or web app) is application software that is accessed using a web browser. Web applications are delivered on the World Wide Web to users with an active network connection.[1]
History
In earlier computing models like client-server, the processing load for the application was shared between code on the server and code installed on each client locally. In other words, an application had its own pre-compiled client program which served as its user interface and had to be separately installed on each user’s personal computer. An upgrade to the server-side code of the application would typically also require an upgrade to the client-side code installed on each user workstation, adding to the support cost and decreasing productivity. In addition, both the client and server components of the application were usually tightly bound to a particular computer architecture and operating system and porting them to others was often prohibitively expensive for all but the largest applications (Nowadays, native apps for mobile devices are also hobbled by some or all of the foregoing issues).[dubious – discuss]
In 1995, Netscape introduced a client-side scripting language called JavaScript allowing programmers to add some dynamic elements to the user interface that ran on the client side. So instead of sending data to the server in order to generate an entire web page, the embedded scripts of the downloaded page can perform various tasks such as input validation or showing/hiding parts of the page.[2]
In 1999, the «web application» concept was introduced in the Java language in the Servlet Specification version 2.2. [2.1?].[3][non-primary source needed] At that time both JavaScript and XML had already been developed, but Ajax had still not yet been coined and the XMLHttpRequest object had only been recently introduced on Internet Explorer 5 as an ActiveX object.[citation needed]
In 2005, the term Ajax was coined, and applications like Gmail started to make their client sides more and more interactive. A web page script is able to contact the server for storing/retrieving data without downloading an entire web page.[4]
Structure
Traditional PC applications consist only of 1 tier, which resides on the client machine, but web applications lend themselves to an multi-tiered approach by nature.[5] Though many variations are possible, the most common structure is the three-tiered application.[5] In its most common form, the three tiers are called presentation, application and storage. A web browser is the first tier (presentation), an engine using some dynamic Web content technology (such as ASP, CGI, ColdFusion, Dart, JSP/Java, Node.js, PHP, Python or Ruby on Rails) is the middle tier (application logic), and a database is the third tier (storage).[5] The web browser sends requests to the middle tier, which services them by making queries and updates against the database and generates a user interface.
For more complex applications, a 3-tier solution may fall short, and it may be beneficial to use an n-tiered approach, where the greatest benefit is breaking the business logic, which resides on the application tier, into a more fine-grained model.[5] Another benefit may be adding an integration tier that separates the data tier from the rest of tiers by providing an easy-to-use interface to access the data.[5] For example, the client data would be accessed by calling a «list_clients()» function instead of making an SQL query directly against the client table on the database. This allows the underlying database to be replaced without making any change to the other tiers.[5]
There are some who view a web application as a two-tier architecture. This can be a «smart» client that performs all the work and queries a «dumb» server, or a «dumb» client that relies on a «smart» server.[5] The client would handle the presentation tier, the server would have the database (storage tier), and the business logic (application tier) would be on one of them or on both.[5] While this increases the scalability of the applications and separates the display and the database, it still doesn’t allow for true specialization of layers, so most applications will outgrow this model.[5]
Security
Security breaches on these kinds of applications are a major concern because it can involve both enterprise information and private customer data. Protecting these assets is an important part of any web application and there
are some key operational areas that must be included in the development process.[6] This includes processes for authentication, authorization, asset handling, input, and logging and auditing. Building security into the applications from the beginning can be more effective and less disruptive in the long run.
Development
Writing web applications is simplified with the use of web application frameworks. These frameworks facilitate rapid application development by allowing a development team to focus on the parts of their application which are unique to their goals without having to resolve common development issues such as user management.[7] Many of the frameworks in use are open-source software.
In addition, there is potential for the development of applications on Internet operating systems, although currently there are not many viable platforms that fit this model.[citation needed]
See also
- D3.js
- Software as a service (SaaS)
- Mobile development framework
- Web 2.0
- Web engineering
- Web GIS
- Web services
- Web sciences
- Web widget
References
- ^ «What Is A Web Application?». stackpath.com. Stack Path. Retrieved 2022-08-15.
A web application is a computer program that utilizes web browsers and web technology to perform tasks over the Internet.
- ^ Liam Tung (2020-06-15). «JavaScript creator Eich: My take on 20 years of the world’s top programming language». ZDNet.
- ^ Davidson, James Duncan; Coward, Danny (1999-12-17). Java Servlet Specification («Specification») Version: 2.2 Final Release. Sun Microsystems. pp. 43–46. Retrieved 2008-07-27.
- ^ Jay Hoffmann (2019-03-04). «What Does AJAX Even Stand For?». Retrieved 2021-10-18.
- ^ a b c d e f g h i Petersen, Jeremy (4 September 2008). «Benefits of using the n-tiered approach for web applications».
- ^ «Top Tips for Secure App Development». Dell.com. Archived from the original on 2012-05-22. Retrieved 2012-06-22.
- ^ Multiple (wiki). «Web application framework». Docforge. Archived from the original on 2020-06-20. Retrieved 2010-03-06.
External links
- HTML5 Draft recommendation, changes to HTML and related APIs to ease authoring of web-based applications.
- Web Applications at Curlie
- Web Applications Working Group at the World Wide Web Consortium (W3C)
- PWAs on Web.dev by Google Developers.
- «Web user interface», «Web-based user interface», «WUI» and «Web UI» redirect here.
Horde groupware is an open-source web application.
A web application (or web app) is application software that is accessed using a web browser. Web applications are delivered on the World Wide Web to users with an active network connection.[1]
History
In earlier computing models like client-server, the processing load for the application was shared between code on the server and code installed on each client locally. In other words, an application had its own pre-compiled client program which served as its user interface and had to be separately installed on each user’s personal computer. An upgrade to the server-side code of the application would typically also require an upgrade to the client-side code installed on each user workstation, adding to the support cost and decreasing productivity. In addition, both the client and server components of the application were usually tightly bound to a particular computer architecture and operating system and porting them to others was often prohibitively expensive for all but the largest applications (Nowadays, native apps for mobile devices are also hobbled by some or all of the foregoing issues).[dubious – discuss]
In 1995, Netscape introduced a client-side scripting language called JavaScript allowing programmers to add some dynamic elements to the user interface that ran on the client side. So instead of sending data to the server in order to generate an entire web page, the embedded scripts of the downloaded page can perform various tasks such as input validation or showing/hiding parts of the page.[2]
In 1999, the «web application» concept was introduced in the Java language in the Servlet Specification version 2.2. [2.1?].[3][non-primary source needed] At that time both JavaScript and XML had already been developed, but Ajax had still not yet been coined and the XMLHttpRequest object had only been recently introduced on Internet Explorer 5 as an ActiveX object.[citation needed]
In 2005, the term Ajax was coined, and applications like Gmail started to make their client sides more and more interactive. A web page script is able to contact the server for storing/retrieving data without downloading an entire web page.[4]
Structure
Traditional PC applications consist only of 1 tier, which resides on the client machine, but web applications lend themselves to an multi-tiered approach by nature.[5] Though many variations are possible, the most common structure is the three-tiered application.[5] In its most common form, the three tiers are called presentation, application and storage. A web browser is the first tier (presentation), an engine using some dynamic Web content technology (such as ASP, CGI, ColdFusion, Dart, JSP/Java, Node.js, PHP, Python or Ruby on Rails) is the middle tier (application logic), and a database is the third tier (storage).[5] The web browser sends requests to the middle tier, which services them by making queries and updates against the database and generates a user interface.
For more complex applications, a 3-tier solution may fall short, and it may be beneficial to use an n-tiered approach, where the greatest benefit is breaking the business logic, which resides on the application tier, into a more fine-grained model.[5] Another benefit may be adding an integration tier that separates the data tier from the rest of tiers by providing an easy-to-use interface to access the data.[5] For example, the client data would be accessed by calling a «list_clients()» function instead of making an SQL query directly against the client table on the database. This allows the underlying database to be replaced without making any change to the other tiers.[5]
There are some who view a web application as a two-tier architecture. This can be a «smart» client that performs all the work and queries a «dumb» server, or a «dumb» client that relies on a «smart» server.[5] The client would handle the presentation tier, the server would have the database (storage tier), and the business logic (application tier) would be on one of them or on both.[5] While this increases the scalability of the applications and separates the display and the database, it still doesn’t allow for true specialization of layers, so most applications will outgrow this model.[5]
Security
Security breaches on these kinds of applications are a major concern because it can involve both enterprise information and private customer data. Protecting these assets is an important part of any web application and there
are some key operational areas that must be included in the development process.[6] This includes processes for authentication, authorization, asset handling, input, and logging and auditing. Building security into the applications from the beginning can be more effective and less disruptive in the long run.
Development
Writing web applications is simplified with the use of web application frameworks. These frameworks facilitate rapid application development by allowing a development team to focus on the parts of their application which are unique to their goals without having to resolve common development issues such as user management.[7] Many of the frameworks in use are open-source software.
In addition, there is potential for the development of applications on Internet operating systems, although currently there are not many viable platforms that fit this model.[citation needed]
See also
- D3.js
- Software as a service (SaaS)
- Mobile development framework
- Web 2.0
- Web engineering
- Web GIS
- Web services
- Web sciences
- Web widget
References
- ^ «What Is A Web Application?». stackpath.com. Stack Path. Retrieved 2022-08-15.
A web application is a computer program that utilizes web browsers and web technology to perform tasks over the Internet.
- ^ Liam Tung (2020-06-15). «JavaScript creator Eich: My take on 20 years of the world’s top programming language». ZDNet.
- ^ Davidson, James Duncan; Coward, Danny (1999-12-17). Java Servlet Specification («Specification») Version: 2.2 Final Release. Sun Microsystems. pp. 43–46. Retrieved 2008-07-27.
- ^ Jay Hoffmann (2019-03-04). «What Does AJAX Even Stand For?». Retrieved 2021-10-18.
- ^ a b c d e f g h i Petersen, Jeremy (4 September 2008). «Benefits of using the n-tiered approach for web applications».
- ^ «Top Tips for Secure App Development». Dell.com. Archived from the original on 2012-05-22. Retrieved 2012-06-22.
- ^ Multiple (wiki). «Web application framework». Docforge. Archived from the original on 2020-06-20. Retrieved 2010-03-06.
External links
- HTML5 Draft recommendation, changes to HTML and related APIs to ease authoring of web-based applications.
- Web Applications at Curlie
- Web Applications Working Group at the World Wide Web Consortium (W3C)
- PWAs on Web.dev by Google Developers.
Всего найдено: 31
Уважаемая Справка, твердо знаю, что в отношении Украины употребляются предлоги НА — С. На уважаемом интернет-ресурсе — «ситуация в Украине». Ошиблись?
Ответ справочной службы русского языка
Скорее, просто решили перестраховаться, чтобы не обижались украинские читатели.
Правильно ли говорить и писать «Интернет-ресурсы»? Коллега утверждает, что это ошибка и можно только «Интернет-ресурс».
Заранее спасибо
Ответ справочной службы русского языка
Сочетание интернет-ресурсы корректно (слово ресурсы здесь синонимично словам сайты, порталы, проекты).
1) Как правильно пишутся слова типа: веб-приложение (веб приложение), видеокарта (видео карта), аудиофайл (аудио файл); интернет-ресурсы (интернет ресурсы) и т.п.
2) На какие справочники при этом ссылаться?
Ответ справочной службы русского языка
Верно: веб-приложение, видеокарта, аудиофайл, интернет-ресурсы. Все эти слова (или первые части сложных слов веб-…, видео…, аудио…) Вы можете найти в орфографическом словаре на нашем портале.
Добрый день! Скажите, пожалуйста, нужны ли в этом предложении какие-либо знаки препинания:
Для удобства просмотра с экрана телевизора специалисты компании адаптируют Интернет-ресурсы под телевизионную систему управления и отображения.
Ответ справочной службы русского языка
Никакие знаки препинания (кроме точки в конце) не нужны. Обратите внимание на написание: интернет-ресурсы (со строчной).
Почему в слове «интернет-ресурс», «интернет» пишется с маленькой буквы???
Ответ справочной службы русского языка
Такова орфографическая норма: в качестве первой части сложных слов интернет- пишется со строчной.
Добрый день! Подскажите, пожалуйста, возможно ли употреблением местоимений нас, нами на информационных интернет-ресурсах с большой буквы? если да, в каких случаях?
Ответ справочной службы русского языка
Местоимение нас, нами с большой буквы не пишется.
Здравствуйте, знаю, Вы уже как-то отвечали на этот вопрос.
Какие Вы можете посоветовать компетентные сайты по украинскому правописанию?
Спасибо.
Ответ справочной службы русского языка
Можем посоветовать Вам материалы лингвистического портала MOVA.info (на портале есть ссылки на интернет-ресурсы, посвященные украинскому языку).
Доброго времени суток!
В связи с возникшей необходимостью обращаюсь к вам с просьбой дать компетентный ответ на вопрос, связанный со склонением моей фамилии в русском языке… Моя фамилия Бальбоа, имеет испанское происхождение, и она не склоняется в русском языке, насколько мне известно… Дело в том, что один из известных интернет-ресурсов требует обоснования данного утверждения, и критерием адекватности данного обоснования служит мнение специалистов вашего ресурса! Заранее благодарен за будущий ответ!
С уважением, Антон Бальбоа…
Ответ справочной службы русского языка
Да, Ваша фамилия не склоняется. Мужские и женские фамилии, оканчивающиеся на -а с предшествующей гласной, не склоняются (независимо от происхождения фамилии и от того, на какой слог падает ударение в фамилии). См., например: Калакуцкая Л. П. Фамилии. Имена. Отчества. Написание и склонение. М., 1994.
мне задали домашнею работу но я не пойму как её делать помогите .вот такое задание ! Объясните значение следующих слов и словосочетаний. Выделите синонимичные пары. Определите новые значения (старых)слов.Найдите новые слова (неологизмы) и объясните их происхождение. Сделайте вывод об употреблении слова Интернет при образовании новых слов. Составьте и запишите 4-5 предложений с даными словами.
Портал, сайт, лингвистическая игра -языковая игра, консультация, открыт, интернет-ресурсы
Ответ справочной службы русского языка
Задание не сложное. Начните с определений, их можно найти в онлайновых словарях и энциклопедиях.
Здравствуйте! Часто встречаю слово Интернет пишушимся как с маленькой, так и с заглавной буквы. Как правильно и почему?
Ответ справочной службы русского языка
Существительное Интернет пишется с большой (прописной) буквы, однако сложные слова с первой частью интернет-… пишутся строчными буквами (интернет-портал, интернет-ресурс).
Здравствуйте!
Подскажите, пожалуйста, что делать, если в конце предложения находится ссылка на интернет-ресурс? С одной стороны, в конце предложения должна стоять точка, с другой — адреса страниц в Интернете на конце точки не имеют, и набрав адрес с точкой, ты вообще на эту страницу не попадешь.
Пример : Адрес сайта: http://www.gramota.ru(.)
Спасибо!
Ответ справочной службы русского языка
Самый лучший вариант — перестроить предложение так, чтобы ссылка находиласть не в конце. Можно также подчеркнуть ссылку (при этом точка останется неподчеркнутой).
Подскажите, пожалуйста, «интернет» стоит писать с заглавной или со строчной буквы? И почему именно так? Спасибо.
Ответ справочной службы русского языка
Интернет пишется с прописной буквы (как имя собственное – название компьютерной сети). Первая часть сложных слов интернет… пишется со строчной буквы: интернет-ресурс, интернет-провайдер.
Скажите, пожалуйста, как все же правильно писать слово интернет, с заглавной или строчной буквы? Предполагаю, что этот вопрос уже не раз задавался, но поиск по ключевым словам, к сожалению, не работает. Прошу этот момент тоже принять во внимание.
Заранее спасибо!
Ответ справочной службы русского языка
Слово Интернет пишется с прописной буквы. Но со строчной буквы пишется первая часть сложных слов интернет-, например: интернет-ресурс, интернет-провайдер.
интернет-сайт, интернет-ресурсы, интернет-технологии. Вы рекомендуете иисать всё с маленькой буквы — в других источниках я встречал написание Интернет- с большой буквы. Пожалуйста, внесите ясность, мы издаём учебники для студентов, и не хотелось бы, чтобы там были ошибки. Большое спасибо!
Ответ справочной службы русского языка
Ответы даны по «Русскому орфографическому словарю» РАН: _Интернет_, но: _интернет-технологии_. Подробные объяснения читайте в статье профессора В. В. Лопатина http://www.gramota.ru/mag_arch.html?id=366 [«Как писать слово Интернет?»].
Скажите, пожалуйста, как правильно писать слово Интернет — с большой или маленькой буквы — интернет-ресурсы, например?
Ответ справочной службы русского языка
Правильно: _Интернет, интернет-ресурсы_.
Ещё 15–20 лет назад нельзя было предположить, что веб-приложения станут неотъемлемой частью жизни. Сегодня эта разновидность онлайн-инструментов используется для различных задач, включая оптимизацию бизнес-процессов, продажу товаров и услуг, распространение информации, общение пользователей друг с другом.
В нашей статье мы попытались подробно рассказать вам о веб-приложениях: по каким принципам работают эти инструменты, какие виды веб-приложений бывают, и как осуществляется разработка веб-приложений в соответствии с современным подходом.
Веб-приложение как инструмент развития бизнеса
В отличие от стандартных приложений, речь идет о программах, которые способны работать полноценно даже без установки на устройство. Смартфон, планшет или компьютер получает онлайн-доступ к данным, а пользователю не нужно проводить установку в постоянную память — это и есть главная отличительная черта веб-приложений.
Эту разновидность инструментов нельзя путать с сайтами. Веб-приложения интерактивны, пользователи могут совершать в них различные действия: заказать товар или услугу, забронировать билет, оставить комментарий или отзыв, редактировать контент и так далее. Примерами веб-приложений могут быть в том числе полноценные онлайн-редакторы, какие как «Документы» от Google или система управления контентом сайта (CMS) «Tilda».
Создание веб-приложения — это на сегодня однозначно один из самых прогрессивных путей инвестирования времени и ресурсов в развитие компании, ведь его внедрение позволяет:
- автоматизировать бизнес-процессы и для сотрудников, и для персонала;
- привлечь внимание целевой аудитории и выделиться на фоне конкурентов;
- сделать решение востребованных задач проще, надежнее и безопаснее.
Красноречивый пример — платформа электронной коммерции Shopify, которая была создана в 2004 году группой энтузиастов, желавших продавать спортивный инвентарь. На тот момент было трудно вообразить, что Shopify станет одной из крупнейших онлайн-платформ, благодаря которым будет развиваться электронная коммерция. Как зачастую бывает в подобных случаях, команда первопроходцев смогла решить собственные задачи — и впоследствии создала основу для достижения целей, которые часто ставят перед собой другие предприниматели.
Принципы работы веб-приложений
Функциональность веб-приложений подразумевает, что они могут работать с несколькими разновидностями страниц, среди которых:
- статистические — серверная часть создаёт страницу в ответ на запрос и отправляет её в браузер вне зависимости от действий пользователя, так что разные пользователи увидят по одному и тому же запросу одинаковый материал;
- динамические — серверная часть формирует страницу в ответ на запрос, только материал предварительно проходит через сервер приложений и формируется в зависимости от того, какие команды были отправлены.
Вне зависимости от того, существует веб-приложение для электронной коммерции, коммуникации, создания контента или других целей, данная разновидность приложений работает по клиент-серверному принципу. Именно поэтому в структуре выделяют следующие компоненты:
- клиентская часть — отвечает за действия, выполняемые пользователем;
- серверная часть — отвечает за процессы, происходящие на сервере;
- база данных — структура для упорядоченного хранения информации и доступа к ней.
В зависимости от того, какие задачи ставят перед собой создатели проекта, они используют те или иные средства разработки веб приложений. Главная задача — обеспечить функциональное взаимодействие между клиентской и серверной частью, доступ к базе данных, корректные возможности по формированию и отправке готовых страниц в ответ на запрос.
Исходя из поставленных задач, разработчики могут создать веб-приложение, к которому удастся получить доступ с любого устройства, или же требовательную среду разработки, для работы с которой подойдут только устройства с определенным уровнем аппаратных возможностей. Могут применяться различные методы разработки веб-приложений, в том числе с открытым доступом к архитектуре, как в «Википедии», или с отсутствием такого доступа для посторонних, как в любом коммерческом или новостном приложении.
Виды веб-приложений
Исходя из того, чем характеризуется проект, его можно классифицировать по нескольким главным признакам.
По шаблону построения сайта
Здесь выделяют веб-приложения нескольких категорий:
- многостраничные (MPA) — запрос отправляется на сервер, а страница полностью обновляется в результате ответа, заменяется на новую;
- одностраничные (SPA) — после отправки запроса на сервер обновляется часть той страницы, из которой состоит приложение, без полной перезагрузки;
- прогрессивные (PWA) — сохраняют свою функциональность, даже когда работают в режиме офлайн из-за отключения доступа к интернету.
В зависимости от того, какая среда разработки используется, можно получить шаблон сайта с заданными параметрами.
По предназначению
Современные веб-приложения могут выполнять множество разных функций. Например, это могут бытькорпоративные порталы, CRM (customer relationship management), ERP (enterprise resource planning), CMS (content management system), электронные коммерческие системыи так далее.
По используемым компонентным моделям
В этой категории выделяют несколько разновидностей:
- Абсолютно без компонентных моделей. Нередко простые скриптовые языки разработки помогают создать программу, которую условно относят к стилю CGI, хотя она является полноценным приложением.
- С универсальными компонентными моделями, которые не предназначены для одних лишь веб-приложений. Например, с помощью COM/ActiveX-объектов на платформе Windows удавалось расширить функциональность веб-сервера, реализовать ту или иную бизнес-логику.
- Со специализированными компонентными моделями. Примером являются сервлеты и документы — универсальные компоненты в мире разработки на языке Java. Управляет этими компонентами особый элемент — веб-контейнер.
В зависимости от того, какая компонентная модель используется, оптимизация веб-приложений также проводится по совершенно иным принципам. Подходы могут существенно различаться исходя из функциональности и конечных целей.
Профессиональная разработка веб-приложений
В случае с каждым таким проектом совершается определенный цикл действий, в котором можно выделить следующие этапы:
- Сбор требований и разработка ТЗ. Заказчик озвучивает как основные задачи, так и более глобальные цели, а также дополняет это своими требованиями, чтобы была возможность ознакомить разработчиков с заданием.
- Прототипирование. Исполнитель создаёт прототип будущего проекта, где отражены будущие блоки и показано, как они будут взаимодействовать в web-среде. Важно выбрать надёжного и опытного исполнителя. Также на данном этапе определяются необходимые технологии разработки.
- Создание дизайна. Разработчики создают макет внешнего вида, чтобы согласовать его с заказчиком вслед за функциональным прототипом.
- Верстка и разработка. Теперь команда приступает к созданию страниц в том виде, в каком они должны быть. Здесь происходит два отдельных процесса: с точки зрения backend важно согласовать выполнение функций, а с точки зрения frontend — реакцию визуальных элементов на действия пользователя.
- Тестирование. Тестировщикам нужно убедиться, что веб-приложение полностью справляется со своими функциями.
- Документирование. На основе уже готового проекта создается документация, которая будет необходима пользователям, чтобы как можно быстрее освоить всю функциональность проекта.
От того, насколько профессионально команда будет подходить к выполнению перечисленных этапов работы, напрямую зависит результат. Важно сразу подобрать таких исполнителей, которые отлично будут понимать свои задачи.
Успех в достижении поставленных целей можно определить в зависимости от того, насколько заказчик доволен и в каком объеме он получит те функции, которые ему были нужны от проекта изначально. Платформы разработки приложений открывают для этого совершенно разные возможности — важно выбрать тот инструментарий, который поможет справиться с намеченной целью на 100%. С точки зрения электронной коммерции, веб-приложения имеют несколько заметных преимуществ:
- Безопасность. Минимальный доступ к серверным элементам и базам данных. А значит, меньше всего можно опасаться взлома и других негативных последствий.
- Доступ с разных устройств. Современные движки позволяют получать доступ к веб-приложению параллельно с разных платформ, например с компьютера или ноутбука на Windows, с мобильных устройств на Android и Apple.
- Отсутствие клиентского ПО. Не нужно расходовать лишние ресурсы — место и память на установку клиентского ПО на устройство.
- Масштабируемость — веб-приложение способно справляться с нужным объёмом задач в зависимости от их количества.
Обслуживать веб-проект намного проще, чем клиентское приложение. Особенно когда выполнены поставленные задачи и предоставлена документация.
Куда обратиться за качественной веб-разработкой?
Таким образом, веб-приложения — инструмент, с помощью которого удается достигать деловых, информационных, социальных целей с минимумом усилий и затрат.
Для разработки веб-приложения понадобится помощь специалистов. Используя современный инструментарий, команда опытных разработчиков может без особого труда справиться с проектом любой сложности. Заказчик останется доволен полученным результатом и сможет перейти к реализации дальнейших целей, которые ставит перед собой в рамках развития своего проекта.
Мы создаем адаптивные и функциональные веб-продукты, используя самые современные технологии в сфере разработки программного обеспечения, и с нетерпением готовы найти лучшее решение для вашей задачи. Чтобы обсудить оптимальные варианты реализации вашего проекта, оставьте заявку на нашем сайте.
Если вам понравилась наша статья…
Ставьте лайк и подписывайтесь на наши обновления, впереди вас ждет еще много всего интересного)
В разработке веб-приложений много моментов, понимание которых приходит только с опытом, поэтому выгодно учиться у тех, кто уже наступил на все возможные грабли и выработал наиболее эффективные способы разработки. Мы попросили экспертов поделиться своим мнением о том, как подойти к разработке веб-приложений. Их ответы публикуем ниже.
Итак, как подойти к разработке веб-приложений?
- Не спешите писать код. Сначала обдумайте архитектуру приложения, подумайте, что будет с продуктом через год-два после создания, заложите масштабируемость, гибкость и т. д.;
- Не стремитесь использовать новые технологии, о которых все говорят. Вы всю жизнь писали на REST и вдруг решили, что вам нужен GraphQL? Сначала ответьте себе, какие у него преимущества перед привычным вам подходом и не перевешивают ли они недостатки. То же касается и других технологий;
- Не забывайте про безопасность. Можно долго выбирать, как передавать данные — с помощью REST или GraphQL — но какая разница, если они будут передаваться по HTTP?
- Создавайте прототипы, чтобы быстро видеть результат своих действий и как можно скорее получать обратную связь от заказчика/пользователя;
- Стоите перед выбором фреймворка? Выбирайте то, на чём лучше умеете писать и что лучше подходит под вашу задачу. Ни на чём не умеете? Тогда выберите что-то, в чём проще и быстрее разобраться;
- Пишите тесты, чтобы быть уверенным в том, что у вас всё работает как надо и очередная фича не стала багом;
- Логируйте работу системы, чтобы потом было проще разобраться в причине неполадок;
- Подумайте об уменьшении количества сторонних зависимостей и попробуйте реализовать нужную функциональность сами. Да, это, скорее всего, займёт больше времени, зато вам не будет мешать никакая сторонняя «магия», и вы будете лучше ориентироваться в коде;
- Прочитайте о 12-факторном приложении. Это что-то вроде набора методологий по созданию веб-приложений.
Напоминаем, что вы можете задать свой вопрос экспертам, а мы соберём на него ответы, если он окажется интересным. Вопросы, которые уже задавались, можно найти в списке выпусков рубрики. Если вы хотите присоединиться к числу экспертов и прислать ответ от вашей компании или лично от вас, то пишите на experts@tproger.ru, мы расскажем, как это сделать.
Значение слова «веб-приложение»
- Веб-приложение — клиент-серверное приложение, в котором клиентом выступает браузер, а сервером — веб-сервер. Логика веб-приложения распределена между сервером и клиентом, хранение данных осуществляется, преимущественно, на сервере, обмен информацией происходит по сети. Одним из преимуществ такого подхода является тот факт, что клиенты не зависят от конкретной операционной системы пользователя, поэтому веб-приложения являются кроссплатформенными сервисами. Веб-приложения стали широко популярными в конце 1990-х — начале 2000-х годов.
Источник: Википедия
-
веб-приложе́ние
1. информ. программа, запускаемая через браузер
Источник: Викисловарь
Делаем Карту слов лучше вместе
Привет! Меня зовут Лампобот, я компьютерная программа, которая помогает делать
Карту слов. Я отлично
умею считать, но пока плохо понимаю, как устроен ваш мир. Помоги мне разобраться!
Спасибо! Я стал чуточку лучше понимать мир эмоций.
Вопрос: электрошокер — это что-то нейтральное, положительное или отрицательное?
Синонимы к слову «веб-приложение»
Предложения со словом «веб-приложение»
- Прочитав эту книгу, вы станете настоящим профессионалом и ценным сотрудником для коммерческих фирм, занятых разработкой веб-приложений различного назначения.
- Появление классификации угроз безопасности веб-приложений является исключительно важным событием в мире IT.
- За последние несколько лет индустрия безопасности веб-приложений адаптировала немалое количество не совсем точных терминов, описывающих уязвимости.
- (все предложения)
Понятия, связанные со словом «веб-приложение»
-
Общий ресурс, или общий сетевой ресурс, — в информатике, это устройство или часть информации, к которой может быть осуществлён удалённый доступ с другого компьютера, обычно через локальную компьютерную сеть или посредством корпоративного интернета, как если бы ресурс находился на локальной машине.
-
Технология единого входа (англ. Single Sign-On) — технология, при использовании которой пользователь переходит из одного раздела портала в другой без повторной аутентификации.
-
Динамический сайт — сайт, состоящий из динамичных страниц — шаблонов, контента, скриптов и прочего, в большинстве случаев в виде отдельных файлов (в Lotus Notes/Domino данные и все элементы дизайна, включая пользовательские скрипты, хранятся в одном файле).
-
Просмотр событий (англ. Event Viewer) — компонент, включённый в состав операционных систем семейства Windows NT, разрабатываемых Microsoft, который позволяет администраторам просматривать лог событий на локальном компьютере или на удалённой машине. В Windows Vista Microsoft переработала систему событий.Из-за регулярного предоставления отчётов о незначительных ошибках запуска и обработки событий (которые на самом деле не наносят вреда или урона компьютеру) программное обеспечение часто используется…
-
Многофакторная аутентификация (МФА, англ. multi-factor authentication, MFA) — расширенная аутентификация, метод контроля доступа к компьютеру, в котором пользователю для получения доступа к информации необходимо предъявить более одного «доказательства механизма аутентификации». К категориям таких доказательств относят…
- (все понятия)
Отправить комментарий
Дополнительно
Смотрите также
-
Прочитав эту книгу, вы станете настоящим профессионалом и ценным сотрудником для коммерческих фирм, занятых разработкой веб-приложений различного назначения.
-
Появление классификации угроз безопасности веб-приложений является исключительно важным событием в мире IT.
-
За последние несколько лет индустрия безопасности веб-приложений адаптировала немалое количество не совсем точных терминов, описывающих уязвимости.
- (все предложения)
- броузер
- адаптер
- фолловер
- листинг
- телемаркетинг
- (ещё синонимы…)
- Как правильно пишется слово «веб-приложение»
Зачем нужны веб-приложения
Интернет-магазины, социальные сети, образовательные продукты, фото-, видео- и текстовые редакторы, игры, системы бронирования — все это и есть веб-приложения. Они устроены сложнее, чем обычные информационные сайты. Пользователь — не пассивный читатель, а участник бизнес-процесса, он взаимодействует с компанией. Информационные сайты без интерактивности бизнесу, конечно, тоже нужны, но их возможности ограничены. Например, это просто сайт-визитка.
Веб-приложения могут пригодиться, чтобы:
- Оказывать услуги пользователю в режиме онлайн: продавать товар, записывать на мероприятие, проводить курсы и вебинары.
- Решать внутренние задачи компании. Большим организациям веб-приложение может понадобиться, чтобы координировать сотрудников, выстраивать внутреннюю логистику. Например, с помощью веб-приложений можно проводить онбординг новых сотрудников и налаживать рабочие процессы с подрядчиками.
Разработка сайта для компании без интерактивности не дает всего этого спектра возможностей. Кроме того, именно интерактивность позволяет добавить геймификацию, делать опции для комментирования и общения пользователей. Все это помогает растить комьюнити вокруг бренда и повышать лояльность к компании. Через сайт можно рассказать пользователю о своей компании, но нельзя получить фидбек или оказать полезную услугу. Поскольку бизнес за последние годы стремительно переходит в онлайн-формат, веб-приложения становятся все более популярными. Для многих компаний именно цифровой сервис — основной источник монетизации.
Веб-приложения, которые человек открывает с браузера, как сайты, конкурируют с мобильными приложениями. Возможности и там, и там схожие. Какой вариант выбрать — нужно смотреть в каждом отдельном случае. Например, корпоративным таск-трекером удобнее пользоваться в вебе, а для сервиса доставки еды более актуальна будет разработка мобильного приложения. Есть и технология, которая совмещает два варианта: человек скачивает приложение на телефон, а работает оно из окна браузера — подробнее мы рассмотрим такую архитектуру в следующем разделе.
Виды веб-приложений
Способов разработки приложений много. От выбранного типа приложения будет зависеть цена, сроки и функциональность. Давайте рассмотрим каждый вид и определим, для каких задач будет оптимальна та или иная архитектура.
Прежде всего, приложения можно разделить на кастомные (написанные кодом) и ноукод (собранные в конструкторах). Современные ноукод-редакторы, скажем, Webflow или Bubble позволяют создавать интерактивные решения — к ним можно подключить платежную систему и сделать работающий интернет-магазин. Ноукод выбирают, потому что это быстро и дешево. Но такие решения подходят разве что для MVP или простых задач — например, лендингов или джоб-бордов с информацией о вакансиях и контактами рекрутёра. Функциональность получается очень ограниченной, потому что ноукод-инструменты — это конструктор с фиксированным набором элементов. Производительность таких приложений тоже ниже, чем у кастомных — большой трафик они не выдержат. Поэтому если вам нужен полноценный сервис, с прицелом на большую аудиторию, стоит остановиться на кастомных решениях.
Хотите больше узнать о специфике ноукод-решений и их отличия от классической разработки? Можете прочитать нашу статью: мы подробно разобрали специфику работы с конструкторами.
Приложения, написанные кодом, тоже различаются между собой — по своей архитектуре или системе организации программы. Давайте рассмотрим, какие они бывают.
SPA
Single Page Application — это одностраничное приложение. Для разработки таких приложений используют HTML и JavaScript. По сути, это разработка лендинга, только интерактивного. Но SPA могут быть достаточно сложными. Суть одностраничных приложений в том, что на сервере хранится одна HTML-страница, контент на которой обновляется по мере прокрутки или переходов по ссылкам. То есть когда вы нажимаете на кнопку, вы не переходите на новую страницу — элементы добавляются к уже загруженной. Например, по этому принципу работает Gmail.
👍Плюсы SPA:
- Разрабатывать такие приложения проще, чем многостраничные.
- Приложения работают быстро: контент на странице просто меняется по мере движения пользователя, его загрузка не требует много ресурсов.
- На одной странице проще сделать однообразный дизайн, а пользователь точно не потеряется и не запутается.
👎Минусы SPA:
- Настройка SEO-оптимизации для SPA будет более трудоемкой, чем для других вариантов.
- Трудно гарантировать безопасность таких страниц: они больше подвержены взломам и утечкам, чем MPA и PWA, которые мы рассмотрим ниже.
MPA
Multi Page Application — это многостраничные приложения. Они позволяют пользователю не просто скроллить окно браузера, а переходить между отдельными страницами. И загрузка контента в таких приложениях происходит целыми страницами. Это значит, например, что если пользователь совершил оплату, в SPA подгрузится окошко с подтверждением, а в MPA страница оплаты полностью обновится. Пример многостраничного приложения — интернет-магазин Amazon.
👍Плюсы MPA:
- Простая SEO-оптимизация.
- Более привычный вариант для большинства пользователей, которые привыкли переходить между страницами.
- Если приложение сложное, с большим количеством функций, оно точно должно быть многостраничным. Переход по страницам позволяет пользователю легко попадать на нужные разделы. Скроллить огромное одностраничное приложение в начало, когда внезапно потребовалась какая-то информация оттуда, никому не захочется.
👎Минусы MPA:
- Сложная и более дорогостоящая разработка и дизайн, чем для SPA.
PWA
Progressive Web Application — прогрессивные веб-приложения. Это что-то среднее между разработкой мобильного приложения и вебсайта. PWA можно сразу из браузера установить на главный экран смартфона в обход магазинов приложений. А ещё такие приложения работают офлайн и отправляют push-уведомления, но при этом открываются в браузере. Это возможно благодаря технологии Service Worker — скрипту, через который проходят все взаимодействия между фронтэндом и бэкендом. У этого скрипта есть доступ к кэшу и данным. По сути к большинству сайтов можно дописать Service Worker — и получится PWA. Поэтому часто на эту технологию переходят СМИ — например, The Washington Post.
👍Плюсы PWA:
- Сочетание офлайн- и онлайн- режима удобно для пользователя.
- Такие приложения можно сравнительно быстро разработать.
👎Минусы PWA:
- Приложение может работать с перебоями в старых версиях браузера.
- Есть браузеры, которые вообще не поддерживают подобные приложения — например, Firefox.
SPA | MPA | PWA | |
Плюсы | — Экономия времени и бюджета
— Приложение быстро работает — Легко сделать однообразный дизайн |
— Простая SEO-оптимизация
— Привычная структура с переходом между страницами |
— Сочетание офлайн- и онлайн- режима
— Доступно с мобильных устройств |
Минусы | — Сложная SEO-оптимизация
— Высокий риск взлома и утечки данных |
— Сложная и более дорогостоящая разработка и дизайн, чем для SPA | — Работает с перебоями в старых версиях браузера, не работает в Firefox |
Мы работаем на платформе Node.js для разработки бэкенда и React — для фронтенда.
С помощью такого стека можно реализовать любой вид архитектуры, который мы рассмотрели: от разработки лендингов до многостраничных и прогрессивных приложений. Дальнейшая работа зависит от конкретных бизнес-задач.
Кто занимается разработкой
Нанимать свою команду для разработки приложения — долго и дорого. По данным Indeed.comhttps://www.indeed.com/hire/c/info/cost-of-hiring-employees?hl=en&co=US, расходы на найм одного сотрудника начинаются от $4000 без учёта зарплат. Чтобы сэкономить, можно обратиться за веб-приложениями к фрилансерам. Но зачастую это не лучшее решение. Для полноценного приложения нужна команда из аналитиков, дизайнеров и программистов, и вряд ли один человек будет обладать всеми знаниями сразу. А если вы нанимаете нескольких фрилансеров, вам придётся брать на себя работу менеджера проектов. Так что самый простой способ разработать сайт для компании — обратиться в студию, где над проектом будет работать опытная и слаженная команда.
В процессе создания веб-приложения участвуют:
- Аналитики — они помогают лучше изучить нишу, определить целевую аудиторию и понять, какая функциональность необходима для успеха на рынке.
- UX/UI-дизайнеры. Продумывают пользовательский путь и создают прототипы. Потом отрисовывают непосредственно дизайн сайта: кнопки, иконки и другие элементы интерфейса.
- Фронтенд-разработчики. Превращают дизайн сайта (макет) в работающий сайт: оживляют кнопки и блоки с помощью кода.
- Бэкенд-разработчики. Отвечают за функциональность: подключают базы данных, платежные системы, выстраивают внутреннюю логику приложения.
- Тестировщики. Проверяют приложение на баги и помогают выпустить на рынок продукт, который работает без ошибок.
Этапы разработки
Каждый специалист отвечает за свой этап в разработке веб-приложения. Тем не менее, этапы и их последовательность в разных студиях могут незначительно различаться. Мы расскажем о том, как устроен процесс в Purrweb и какое участие владельцу бизнеса нужно будет принимать в процессе.
Аналитика
В студию стоит идти, когда у вас уже есть готовая идея приложения и примерное понимание того, как оно будет функционировать. Для этого у предпринимателя должна быть экспертиза в выбранной отрасли. А вот изучение рынка можно делегировать специалистам. Аналитики разберутся, какую нишу может занять ваш продукт на рынке, на какую целевую аудиторию выгоднее ориентироваться, какая функциональность нужна этим людям и какую модель монетизации выбрать.
Результат: после работы с аналитиком вы будете четко понимать, для кого делаете приложение, какие задачи пользователей оно будет решать и как на этом зарабатывать.
UX-дизайн
Современные пользователи привыкли к продуманным и качественным интерфейсам. В условиях высокой конкуренции брендов, если что-то в приложении покажется человеку неудобным или непонятным, он просто перейдет по другой ссылке в поисковике. Поэтому важно продумать путь пользователя: какую последовательность действий он будет совершать и как элементы сайта будут отзываться на эти действия. UX-дизайнер создает прототип — на нем обозначены основные блоки и элементы, показано взаимодействие между ними.
Результат: Готовая схема приложения, в которой показаны блоки интерфейса и взаимосвязи между ними. По прототипу вы сможете оценить функциональность приложения и его доступность.
UI-дизайн
Задача дизайнера интерфейса — визуализировать блоки, которые придумал UX-дизайнер. UI-дизайн — это отрисовка иконок и кнопок, вёрстка экранов, подбор цветов и шрифтов и подготовка руководства по стилю UI для дальнейшей разработки. Если вы хотите узнать больше о работе UI-дизайнера, прочитайте наши статьи — о дизайн-процессе и о том, как мы делаем руководства по стилю для наших проектов.
Результат: вы увидите почти готовое приложение, только еще не работающее. Именно после этапа дизайна сайта можно утверждать точную стоимость и сроки разработки. Они будут зависеть от сложности проекта и пожеланий клиента по функциям и дизайну.
Фронтенд
Разработчик фронтенда отвечает за внешнюю часть сайта, которую видят пользователи. Но фронтенд — это не только вёрстка макетов. Фронтенд-разработчик отвечает за адаптивный и отзывчивый дизайн — то есть чтобы сайт корректно отображался на разных устройствах и в браузерах разных версий. На этом этапе определяется процесс загрузки элементов, их кликабельность, анимация и другие микровзаимодействия.
Результат: дизайн становится интерактивным: можно нажимать кнопки, переходить по ссылкам. Но свои функции приложение ещё не выполняет.
Бэкенд
Следующий этап — это разработка внутренней части: как приложение будет работать с базами данных, каким образом будет происходить оплата, бронь и другие процессы. Бэкенд-разработчик отвечает за корректную работу сайта, связь между компонентами приложения, хранение и структуру данных, логику алгоритмов и вычислений, и интеграцию с внешними сервисами — например, с платёжными системами и социальными сетями.
Результат: полностью работающее приложение.
Тестирование
Это необходимый этап, чтобы финальное приложение работало так, как было задумано. Главная задача тестировщика — проверить работу приложения перед релизом, чтобы команда вышла на рынок с качественным продуктом. Тестировщики изучают документацию продукта и составляют тест-кейсы — список функций, которые надо проверить, и их последовательность. Затем они вручную имитируют действия пользователя в разных сценариях или пишут скрипты для автоматического тестирования. После этого разработчики получают отчёт со списком ошибок и рекомендациями по исправлению.
Результат: приложение работает без ошибок, риск их появления сведён к минимуму.
После этого приложение можно запускать для пользователей. Но команда не прекращает над ним работать — она выходит на этап поддержки. После релиза разработчики обеспечивают стабильную работу приложения и чинят баги, найденные пользователями. А ещё на поддержке команда может специально собирать обратную связь от пользователей и улучшать качество продукта — например, добавлять новые фичи или менять дизайн уже готовых разделов приложения.
Стоимость разработки
Стоимость разработки веб-приложения зависит от его функциональности. Давайте рассмотрим самые популярные типы веб-приложений. В конце мы приведем подробную оценку этапов работы над каждым из них.
☝Разумеется, мы приводим только приблизительные цифры: итоговая цена приложения зависит от множества нюансов. Если нужно будет больше или, наоборот, меньше функций, то и стоимость изменится. Обычно финальную сумму можно назвать, когда готов дизайн: тогда уже точно понятно, как будет выглядеть приложение и что в нем будет.
Социальная сеть, в которой реализованы:
- чат;
- лента новостей с комментариями;
- публичные страницы или форумы;
- профиль пользователя.
Это приложение будет стоить у нас около 10 182 000 рублей.
Сервис-агрегатор для путешественников: с бронированием отелей, ж/д или авиабилетов. В нем должны быть:
- личный кабинет;
- поиск с фильтрами;
- блог с рекомендациями по турам;
- оплата
- рассылка.
Такое веб-приложение будет стоить примерно 7 845 000 рублей.
Вот подробная таблица, в которой расписаны все этапы разработки с количеством часов и стоимостью:
Вид работы | Стоимость для соцсети | Стоимость для агрегатора |
Аналитика | 120 часов / 300.000 р. | 120 часов / 300.000 р. |
UX-дизайн | 260 часов / 910.000 р. | 150 часов / 525.000 р. |
UI-дизайн | ||
Фронтенд | 1600 часов / 5.600.000 р. | 1200 часов / 4.200.000 р. |
Бэкенд | 800 часов / 2.800.000 р. | 600 часов / 2.100.000 |
Тестирование | 540 часов / 972.000 р. | 400 часов / 720.000 р. |
Создание веб-приложения — комплексный процесс, который требует сотрудничества с профессиональной командой. У нас в Purrweb есть большой опыт создания интернет-магазинов и других видов приложений в разных сферах. Например, недавно мы разрабатывали маркетплейс видеоконтента и веб-приложение для фитнеса.
Есть бизнес-идея? Обращайтесь к нам за консультацией!