Домой Edit me on GitHub

2020-12-05

Каналы передачи данных | Сетевое программирование | Базы данных | Основы Веб-программирования

Статика

При загрузке HTML страницы браузер ищет все недостающие медиа-файлы (css, js, swf, png, svg, gif, jpg, avi, mp3, mp4, …) и подгружает их отдельными HTTP запросами (см. itcase).

На картинке выше видно, что FireFox нашел 14 дополнительных ресурсов для этого сайта. Браузер автоматически установит 14 TCP соединений, по одному на каждый ресурс, и попытается получить данные, отправив HTTP запросы. Т.к. установка соединения и сетевые задержки — очень дорогие операции, лучшей практикой является объединение ресурсов в один файл, например можно объединить все JavaScript файлы в один при помощи RequireJS, то же можно проделать и для CSS файлов или для картинок (спрайты), помимо прочего Webpack вообще позволяет запаковывать JavaScript файлы совместно с CSS.

Примечание

Стоит отметить, что протокол HTTP2 лишен этого недостатка, так как загружает ресурсы асинхронно в рамках одного соединения.

Отдачу статических файлов можно настроить через отдельный сервер статики, например Nginx, или реализовать средствами самого Веб-приложения (гораздо медленнее).

Nginx

Для примера возьмем следующую структуру файлов:

/usr/share/nginx/html/
|-- index.html
|-- index2.html
`-- static_example
    `-- static
        |-- html-css-js.png
        |-- jquery.min.js
        |-- script.js
        `-- style.css

2 directories, 6 files

index2.html страница, которая ссылается на другие статические файлы.

index2.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Untitled Document</title>
  <link href="/static/style.css" media="all" rel="stylesheet" type="text/css" />
  <script src="/static/jquery.min.js"></script>
  <script src="/static/script.js"></script>
</head>
<body>
<table width="100%" border="1" cellspacing="10" cellpadding="10">
  <tr>
    <td width="60%">
    <h1>HTML</h1>
    <a href="">HTML</a>
    <a href="">JS</a>
    <a href="" style="color: green">CSS</a>
    <hr/>
    <p class="replace-text">HTML (от англ. HyperText Markup Language — «язык
    гипертекстовой разметки») — стандартный язык разметки документов во
    Всемирной паутине. Большинство веб-страниц содержат описание разметки на
    языке HTML (или XHTML). Язык HTML интерпретируется браузерами и
    отображается в виде документа в удобной для человека форме.</p> <p
    style="color: black; background-color: red; color: #fff"> Язык HTML
    является приложением («частным случаем») SGML (стандартного обобщённого
    языка разметки) и соответствует международному стандарту ISO 8879.</p>
    <p class="hide"> Язык XHTML является более строгим вариантом HTML, он
    следует всем ограничениям XML и, фактически, XHTML можно воспринимать
    как приложение языка XML к области разметки гипертекста.</p> <p> Во
    всемирной паутине HTML-страницы, как правило, передаются браузерам от
    сервера по протоколам HTTP или HTTPS, в виде простого текста или с
    использованием сжатия.</p>
    <hr/>
    </td>
    <td width="40%"><img src="/static/html-css-js.png" class="jquery-image"
      width="500" height="293" alt="HTML JS CSS"></td> </tr>
</table>
</body>
</html>

Сервер Nginx настроен таким образом, что по адресу /example отдается страница index2.html, а по /static файлы из директории /usr/share/nginx/html/static_example/static.

/etc/nginx/sites-enabled/default.nginx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# default.nginx

server {
    listen 80 default_server;

    root /usr/share/nginx/html;
    index index.html index.htm;

    include includes/fcgi.nginx;
    include includes/static.nginx;
}
/etc/nginx/includes/static.nginx
1
2
3
4
5
6
7
location  /example {
    try_files ../$uri ../$uri/ /index2.html;
}

location /static {
    alias /usr/share/nginx/html/static_example/static;
}
../../_images/nginx_wo_static.png

Пример index2.html без статики

Если скопировать файлы статики в директорию /usr/share/nginx/html/static_example/static, то сервер начнет их отдавать:

../../_images/nginx_with_static.png

Пример index2.html со статикой

Paste

Приведем предыдущий пример к следующей структуре файлов:

.
├── app.py
├── index.html
└── static
    ├── html-css-js.png
    ├── jquery.min.js
    ├── script.js
    └── style.css

1 directory, 6 files

Для отдачи статики используется WSGI-приложение StaticURLParser из модуля paste.

app.py
1
2
3
4
5
6
7
from paste.urlparser import StaticURLParser
from paste import httpserver

static_app = StaticURLParser(".")

if __name__ == '__main__':
    httpserver.serve(static_app, host='0.0.0.0', port='8000')

По адресу http://localhost:8000 будет открыта страница index.html (действие по умолчанию). Остальные файлы доступны по адресам:

Блог

В нашем примере блога определенно не хватает стилей и динамики. Чтобы это исправить, добавим файлы css и js в директорию static, как показано ниже:

.
├── __init__.py
├── models.py
├── requirements.txt
├── static
│   ├── main.css
│   └── main.js
├── templates
│   ├── base.html
│   ├── create.html
│   ├── index.html
│   └── read.html
└── views.py

2 directories, 12 files
templates/base.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
    {% block head %}
      <title>{% block title %}{% endblock %} - My Blog</title>
      <meta charset='utf-8'>
    {% endblock %}


    {% block css %}
      <link href="/static/main.css" media="all" rel="stylesheet" type="text/css" />
    {% endblock %}
</head>
<body>
    <div class="wrapper">
      <div class="blog">
        {% block content %}
        {% endblock %}
      </div>
      <div class="footer">
          {% block footer %}
            &copy; Copyright 2015 by <a href="http://domain.invalid/">you</a>.
          {% endblock %}
      </div>
    </div>
    {% block js %}
        <script src="/static/main.js" type="text/javascript"></script>
    {% endblock %}
</body>
</html>
static/main.css
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
 .wrapper
        {
          width: 50%;
          max-width: 600px;
          margin: 0 auto;
          padding: 100px 0 0 0;
          min-width: 550px;
        }
        .blog
        {
          position: relative;
        }
          .blog__title
          {
            font: 1.8em Arial;
          }
            .blog__title-link
            {
              color: blue;
            }
            .blog__title-link:hover
            {
              color: red;
            }
            .blog__title-text
            {
              color: #000;
              position: relative;
              display: inline-block;
              padding: 0 0 0 0.9em;
            }
            .blog__title-text:after
            {
              display: block;
              content: ">";
              position: absolute;
              left: 0;
              top: 0;
            }
          .blog__button
          {
            font: 1.1em Arial;
            display: inline-block;
            background: green;
            color: #fff;
            padding: 0.3em 0.5em;
            text-decoration: none;
            position: absolute;
            right: 3.5%;
            top: 0;
          }
          .blog__button:hover
          {
            background: darkgreen;
          }
          .blog-list
          {
            padding: 1em 0 0 0;
            border-top: solid 1px #ccc;
            margin: 1em 0 0 0;
          }
            .blog-list__item
            {
              padding: 0.5em 1em;
            }
            .blog-list__item:hover
            {
              background: #efefef;
            }
              .blog-list__item-id
              {
                width: 5%;
                font: 0.9em Arial;
                display: inline-block;
              }
              .blog-list__item-link
              {
                width: 65%;
                font: 0.9em Arial;
                display: inline-block;
                color: blue;
              }
              .blog-list__item-link:hover
              {
                color: red;
              }
              .blog-list__item-action
              {
                width: 28%;
                display: inline-block;
                text-align: right;
              }
                .blog-list__item-edit
                {
                  font: 0.9em Arial;
                  display: inline-block;
                  background: blue;
                  color: #fff;
                  padding: 0.3em 0.6em;
                  text-decoration: none;
                }
                .blog-list__item-edit:hover
                {
                  background: darkblue;
                }
                .blog-list__item-delete
                {
                  font: 0.9em Arial;
                  display: inline-block;
                  background: red;
                  color: #fff;
                  padding: 0.3em 0.6em;
                  text-decoration: none;
                }
                .blog-list__item-delete:hover
                {
                  background: darkred;
                }
          .blog-item
          {
            padding: 1em 0 0 0;
            border-top: solid 1px #ccc;
            margin: 1em 0 0 0;
          }
            .blog-item__title
            {
               font: 1.8em Arial;
               color: #000;
               padding: 0 0 0.5em 0;
            }
            .blog-item__text
            {
              font: 0.9em Arial;
              color: #000;
            }
          .blog-form
          {
            padding: 1em 0 0 0;
            border-top: solid 1px #ccc;
            margin: 1em 0 0 0;
          }
            .blog-form-field
            {
              padding: 0 0 1em 0;
            }
              .blog-form-field__title
              {
                font: 0.9em Arial;
                color: #000;
                padding: 0 0 0.5em;
              }
              .blog-form-field__input
              {
                width: 100%;
                border: solid 1px #ccc;
                font: 0.9em Arial;
                padding: 0.3em 0.5em;
                box-sizing: border-box;
              }
              .blog-form-field__textarea
              {
                width: 100%;
                border: solid 1px #ccc;
                font: 0.9em Arial;
                padding: 0.3em 0.5em;
                min-height: 200px;
                box-sizing: border-box;
              }
            .blog-form__button
            {
              font: 1.2em Arial;
              display: inline-block;
              background: blue;
              color: #fff;
              padding: 0.3em 0.6em 0.25em 0.6em;
              text-decoration: none;
              border: solid 0px red;
              cursor: pointer;
            }
            .blog-form__button:hover
            {
              background: darkblue;
            }
          .footer
          {
            padding: 3em 0 0 1em;
            font: 0.8em Arial;
          }

Функция confirm_delete() выводит окно подтверждения при нажатии на кнопку удаления статьи.

static/main.js
1
2
3
function confirm_delete() {
  return confirm("Are you sure to delete entry?");
}

Добавим классы в остальных шаблонах, чтобы наши стили применились.

templates/index.html со стилями.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{% extends "base.html" %}

{% block title %}Index{% endblock %}

{% block content %}
    <div class="blog__title">Simple Blog</div>
    <a href="/article/add" class="blog__button">add article</a>
    <div class="blog-list">
        {% for article in articles %}
            <div class="blog-list__item">
                <div class="blog-list__item-id">{{ article.id }}</div>
                <a href="/article/{{ article.id }}" class="blog-list__item-link">{{ article.title }}</a>
                <div class="blog-list__item-action">
                    <a href="/article/{{ article.id }}/edit" class="blog-list__item-edit">edit</a>
                    <a href="/article/{{ article.id }}/delete" onclick="return confirm_delete();"
                        class="blog-list__item-delete">delete</a>
                </div>
            </div>
        {% endfor %}
    </div>
{% endblock %}
templates/read.html со стилями.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{% extends "base.html" %}

{% block title %}{{ article.title }}{% endblock %}

{% block content %}
    <div class="blog__title">
        <a href="/" class="blog__title-link">Simple Blog</a>
        <span class="blog__title-text">{{ article.title }}</span>
    </div>
    <div class="blog-item">
        <div class="blog-item__title">{{ article.title }}</div>
        <div class="blog-item__text">{{ article.content }}</div>
    </div>
{% endblock %}
templates/create.html со стлями.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{% extends "base.html" %}

{% block title %}Create{% endblock %}

{% block content %}
    <div class="blog__title">
        <a href="/" class="blog__title-link">Simple Blog</a>
        <span class="blog__title-text">Edit</span>
    </div>
    <form action="" method="POST" class="blog-form">
        <div class="blog-form-field">
            <div class="blog-form-field__title">Title:</div>
            <input type="text" class="blog-form-field__input" name="title" value="{{ article.title }}"><br>
        </div>
        <div class="blog-form-field">
            <div class="blog-form-field__title">Content:</div>
            <textarea class="blog-form-field__textarea" name="content">{{ article.content }}</textarea><br><br>
        </div>
        <input class="blog-form__button" type="submit" value="Submit">
    </form>
{% endblock %}

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

../../_images/blog_wo_static.png

Новые шаблоны блога без статики

Статику будет отдавать WSGI-приложение StaticURLParser.

__init__.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from paste.auth.basic import AuthBasicHandler

import selector
from views import BlogCreate, BlogDelete, BlogIndex, BlogRead, BlogUpdate


def authfunc(environ, username, password):
    return username == 'admin' and password == '123'


def make_wsgi_app():
    # BasicAuth applications
    create = AuthBasicHandler(BlogCreate, 'www', authfunc)
    update = AuthBasicHandler(BlogUpdate, 'www', authfunc)
    delete = AuthBasicHandler(BlogDelete, 'www', authfunc)

    # URL dispatching middleware
    dispatch = selector.Selector()
    dispatch.add('/', GET=BlogIndex)
    dispatch.prefix = '/article'
    dispatch.add('/add', GET=create, POST=create)
    dispatch.add('/{id:digits}', GET=BlogRead)
    dispatch.add('/{id:digits}/edit', GET=update, POST=update)
    dispatch.add('/{id:digits}/delete', GET=delete)

    # Static files
    from paste.urlparser import StaticURLParser
    static_app = StaticURLParser("static/")

    from paste import urlmap
    mapping = urlmap.URLMap()
    mapping['/static'] = static_app

    from paste.cascade import Cascade
    app = Cascade([mapping, dispatch])

    return app

if __name__ == '__main__':
    from paste.httpserver import serve
    app = make_wsgi_app()
    serve(app, host='0.0.0.0', port=8000)

При помощи paste.urlmap.URLMap добавляется префикс /static/ для наших файлов из директории static. paste.cascade.Cascade позволяет запускать несколько WSGI-приложений одновременно. Таким образом наш блог принял следующую архитектуру:

../../_images/blog_scheme.png

Структура блога со статикой

И стал по-другому выглядеть:

../../_images/blog_with_static.png

Новые шаблоны блога со статикой

../../_images/blog_delete_action.png

Окно подтверждения при удалении статьи

Для реальных проектов лучше использовать библиотеку Whitenoise, она поддерживает Python3 и CDN.

__init__.py файл блога
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from views import BlogRead, BlogIndex, BlogCreate, BlogDelete, BlogUpdate
from wsgi_basic_auth import BasicAuth

# third-party
import selector


def make_wsgi_app():
    passwd = {
        'admin': '123'
    }
    # BasicAuth applications
    create = BasicAuth(BlogCreate, 'www', passwd)
    update = BasicAuth(BlogUpdate, 'www', passwd)
    delete = BasicAuth(BlogDelete, 'www', passwd)

    # URL dispatching middleware
    dispatch = selector.Selector()
    dispatch.add('/', GET=BlogIndex)
    dispatch.prefix = '/article'
    dispatch.add('/add', GET=create, POST=create)
    dispatch.add('/{id:digits}', GET=BlogRead)
    dispatch.add('/{id:digits}/edit', GET=update, POST=update)
    dispatch.add('/{id:digits}/delete', GET=delete)

    return dispatch

if __name__ == '__main__':

    app = make_wsgi_app()

    from whitenoise import WhiteNoise
    app = WhiteNoise(app)
    app.add_files('./static/', prefix='static/')

    from paste.httpserver import serve
    serve(app, host='0.0.0.0', port=8000)
Previous: Bash Next: Пагинация