Как быстро написать веб-сайт или веб-приложение и не увязнуть в сборщиках

    Это маленькое руководство описывает создание реактивного веб-приложения используя отрисовку на стороне сервера (Server-Side Rendering, SSR). Клиентская часть являет собой полноценное Vue-приложение, в моём случае используя шаблон MVVM. Серверное приложение работает на микрофреймворке Flask, который может предоставить конечные точки подключения (endpoint) и отдать готовую HTML страницу. HTML страницы (расположены в подкаталоге myapp/templates) рендерятся шаблонизатором Jinja (устанавливается в качестве зависимости Flask).

    Внимание: быстро ещё не значит, что статья предназначена для новичков.

    Используемые технологии и фреймворки:


    Для API используем протокол JSON-RPC www.jsonrpc.org/specification. Протокол отличается простотой, удобочитаемостью и без лишних костылей работает как на серверной, так и на клиентской стороне.

    Подготовка


    Установка необходимых пакетов

    pip install flask flask-jsonrpc

    Создаём каталог проекта и подготавливаем структуру внутри. С рекомендуемой структурой приложения можно ознакомиться здесь https://habr.com/ru/post/421887/

    mkdir -p myapp/{myapp/{static/{js,css},ns_api,templates},config,data}
    cd myapp

    Скачиваем нужные файлы JS и CSS фреймворков

    wget -O myapp/static/js/jquery-3.3.1.slim.min.js https://code.jquery.com/jquery-3.3.1.slim.min.js
    wget -O myapp/static/js/popper.min.js https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js
    wget -O myapp/static/js/bootstrap.min.js https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js
    wget -O myapp/static/css/bootstrap.min.css https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css
    wget -O myapp/static/js/vue.min.js https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js
    wget -O myapp/static/js/axios.min.js https://unpkg.com/axios/dist/axios.min.js

    Здесь есть зависимость jquery, но только для работы Bootstrap

    Минимальное Flask приложение


    Файл run.py для ручного старта и тестирования

    #!/usr/bin/env python3
    
    from myapp import app as application
    
    application.run(host='0.0.0.0', port=8000)

    Файл config/default.py для настройки приложения

    import os
    import sys
    
    # Конфигурация
    DEBUG = True
    SQLDEBUG = False
    
    SESSION_COOKIE_NAME = 'myapp'
    SESSION_TYPE = 'filesystem'
    
    TITLE = 'Проект'
    
    DIR_BASE = '/'.join(os.path.dirname(os.path.abspath(__file__)).split('/')[:-1])
    DIR_DATA = DIR_BASE + '/data'
    # Генерировать можно утилитой pwgen
    # Пример:
    # pwgen -sy 64
    SECRET_KEY = '''0123456789'''
    
    # Логирование
    LOG_FILE = DIR_DATA + '/myapp.log'
    LONG_LOG_FORMAT = '%(asctime)s - [%(name)s.%(levelname)s] [%(threadName)s, %(module)s.%(funcName)s@%(lineno)d] %(message)s'
    LOG_FILE_SIZE = 128 # Размер файла лога в МБ


    Файл config/__init__.py

    CONFIG = 'config.default'


    Файл myapp/__init__.py

    import config
    import logging
    
    from flask import Flask
    from logging.handlers import RotatingFileHandler
    
    app = Flask(__name__)
    app.config.from_object(config.CONFIG)
    
    app.config.from_envvar('FLASKR_SETTINGS', silent=True)
    
    # Логирование
    handler = RotatingFileHandler(app.config['LOG_FILE'],
        maxBytes=app.config['LOG_FILE_SIZE']*1024*1024,
        backupCount=1)
    handler.setLevel(logging.INFO)
    formatter = logging.Formatter(app.config['LONG_LOG_FORMAT'])
    handler.setFormatter(formatter)
    app.logger.addHandler(handler)
    
    # API
    from . import ns_api
    
    from . import views

    Файл myapp/ns_api/__init__.py

    from flask_jsonrpc import JSONRPC
    
    from .. import app
    
    jsonrpc = JSONRPC(app, '/api')
    
    from . import logic

    Файл myapp/views.py

    from myapp import app
    from flask import render_template
    
    
    @app.route('/')
    def index():
        pagedata = {}
        pagedata['title'] = app.config['TITLE']
        pagedata['data'] = {
            "A": True,
            "B": False,
            "result": False
        }
        body = render_template('index.html', pagedata=pagedata)
        return body


    Файл myapp/ns_api/logic.py

    import operator
    
    from . import jsonrpc
    
    
    @jsonrpc.method('logic.and(A=bool, B=bool)')
    def logic_and(A, B):
        """
        Логическое И
        """
        return operator.and_(A, B)
    
    
    @jsonrpc.method('logic.not(A=bool)')
    def logic_not(A):
        """
        Логическое НЕ
        """
        return operator.not_(A)
    
    
    @jsonrpc.method('logic.or(A=bool, B=bool)')
    def logic_or(A, B):
        """
        Логическое ИЛИ
        """
        return operator.or_(A, B)
    
    
    @jsonrpc.method('logic.xor(A=bool, B=bool)')
    def logic_xor(A, B):
        """
        Логическое ИСКЛЮЧАЮЩЕЕ ИЛИ
        """
        return operator.xor(A, B)


    Устанавливаем права на запуск

    chmod +x run.py


    Клиентская сторона пользовательского интерфейса (фронтенд, front-end)



    Файл myapp/templates/header.html

    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" type="text/css" href="/static/css/bootstrap.min.css" />
        <script src="/static/js/jquery-3.3.1.slim.min.js"></script>
        <script src="/static/js/popper.min.js"></script>
        <script src="/static/js/bootstrap.min.js"></script>
        <script src="/static/js/vue.min.js"></script>
        <script src="/static/js/axios.min.js"></script>
        <title>{{ pagedata['title'] }}</title>
    </head>


    Файл myapp/templates/skeleton.html

    <!DOCTYPE html>
    <html lang="ru">
        {% include 'header.html' %}
    <body>
    <section id="app">
    
    <div class="container-fluid">
    {% block content %}
    {% endblock %}
    </div>
    
    </section>
    
    {% block script %}
    <script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
        },
        methods: {
        }
    })
    </script>
    {% endblock %}
    
    </body>
    </html>

    Файл myapp/templates/index.html

    {% extends "skeleton.html" %}
    {% block content %}
    
    <h1>Микросервисная архитектура</h1>
    
    <a href="http://127.0.0.1:8000/api/browse">http://127.0.0.1:8000/api/browse</a>
    
    <h2>API</h2>
    
    <pre>curl -i -X POST \
       -H "Content-Type: application/json; indent=4" \
       -d '{
        "jsonrpc": "2.0",
        "method": "logic.and",
        "params": {
            "A": true,
            "B": true
        },
        "id": "1"
    }' http://127.0.0.1:8000/api
    </pre>
    
    <h3>Логические</h3>
    
    <ul>
        <li>logic.and(A, B)</li>
        <li>logic.not(A)</li>
        <li>logic.or(A, B)</li>
        <li>logic.xor(A, B)</li>
    </ul>
    <h3>API</h3>
    <div class="btn-group">
    <div class="btn btn-outline-success" v-if="A" v-on:click="changeA">Истина</div>
    <div class="btn btn-outline-danger" v-else v-on:click="changeA">Ложь</div>
    <div class="btn btn-outline-secondary disabled">И</div>
    <div class="btn btn-outline-success" v-if="B" v-on:click="changeB">Истина</div>
    <div class="btn btn-outline-danger" v-else v-on:click="changeB">Ложь</div>
    <div class="btn btn-outline-secondary disabled">=</div>
    <div class="btn btn-success disabled" v-if="result">Истина</div>
    <div class="btn btn-danger disabled" v-else>Ложь</div>
    </div>
    {% endblock %}
    
    {% block script %}
    <script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {{ pagedata['data']|tojson|safe }},
        methods: {
            changeA: function() {
                var vm = this;
                vm.A = !vm.A;
                vm.update();
            },
            changeB: function() {
                var vm = this;
                vm.B = !vm.B;
                vm.update();
            },
            update: function() {
                var vm = this;
                axios.post(
                    '/api',
                    {
                        "jsonrpc": "2.0",
                        "method": 'logic.and',
                        "params": {
                            "A": vm.A,
                            "B": vm.B
                        },
                        "id": 1
                    }
                ).then(
                    function(response) {
                        if ('result' in response.data) {
                            vm.result = response.data['result'];
                        }
                    }
                );
            }
        }
    })
    </script>
    {% endblock %}
    • +19
    • 8,9k
    • 9
    Поделиться публикацией

    Комментарии 9

      +7
      Когда решил использовать wget, чтобы не увязнуть в сборщиках.
        +2
        vue CLI решает все эти проблемы, плюс в есть полноценный UI чтобы напрягаться с терминалом.
          0
          А если нужно немного серверного рендера то create-nuxt-app может помочь.
          0
          Во-первых, npm — не сборщик. Раз уж вы используете pip, npm тоже не должен быть под запретом :)
          Во-вторых, я как будто вернулся в свой 2011й, с статический генерацией HTML как в PHP.
          А главная моя претензия даже не к Вам, а к авторам Jinja, потому что ваш файл myapp/templates/index.html — это невалидный HTML, и я не понимаю, почему у него оставили формат файла .html. Один из моих старых глупых принципов — это то, что html файл можно открыть в браузере и он хотя бы попытается показать своё содержимое
            –3
            Я даже с вами спорить не буду, хотя бы потому, что у меня нигде не упомянут npm (разве только в пути одной из ссылок).
              0
              для подсветки удобно. если написать там tmplt — то не понятно от чего шаблон, а index.tmplt.html смотрится некрасиво. а так общая практика: также в джанго, например.
              +1

              На всякий случай хочу предостеречь читающих от использования RotatingFileHandler. В продакшене у вас наверняка будет gunicorn/uwsgi, которые запускают несколько процессов для работы приложения. В этом случае ротация будет работать непредсказуемо. Лучше писать логи в stderr и пусть их собирает специализированный инструмент (например journald, если вы запускаете через systemd, докер тоже умеет сбор логов)

                +1
                Не очень понял, что хотел показать в этом примере автор — как сделать сложно простую вещь, или что?
                  0
                  Для чего люди пишут такие статьи — непонятно. Возможно чтобы сделать себе памятку, но зачем ее всем показывать?

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое