При загрузке 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, или реализовать средствами самого Веб-приложения (гораздо медленнее).
Для примера возьмем следующую структуру файлов:
/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
страница, которая ссылается на другие статические файлы.
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
.
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;
} |
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;
} |
Если скопировать файлы статики в директорию
/usr/share/nginx/html/static_example/static
, то сервер начнет их отдавать:
Приведем предыдущий пример к следующей структуре файлов:
.
├── app.py
├── index.html
└── static
├── html-css-js.png
├── jquery.min.js
├── script.js
└── style.css
1 directory, 6 files
Для отдачи статики используется WSGI-приложение StaticURLParser
из модуля paste.
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
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 %}
© 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> |
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()
выводит окно подтверждения при нажатии на кнопку
удаления статьи.
1 2 3 | function confirm_delete() {
return confirm("Are you sure to delete entry?");
} |
Добавим классы в остальных шаблонах, чтобы наши стили применились.
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 %} |
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 %} |
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 %} |
В результате получим новое отображение блога, но пока мы не укажем откуда брать статику, он ее не сможет найти.
Новые шаблоны блога без статики
Статику будет отдавать WSGI-приложение StaticURLParser
.
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-приложений одновременно. Таким образом наш блог принял
следующую архитектуру:
И стал по-другому выглядеть:
Новые шаблоны блога со статикой
Окно подтверждения при удалении статьи
Для реальных проектов лучше использовать библиотеку Whitenoise, она поддерживает Python3 и CDN.
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) |