В программном обеспечении принято разделять: программную логику, код, который относится к данным, настройки и шаблоны. Таким образом, код становится более структурированным, и его легче развивать, а также сопровождать в дальнейшем.
MVC (Model-View-Controller: модель-вид-контроллер) — шаблон архитектуры ПО, который подразумевает разделение программы на 3 слабосвязанных компонента, каждый из которых отвечает за свою сферу деятельности.
Бешеная популярность данной структуры в Веб-приложениях сложилась благодаря её включению в две среды разработки, которые стали очень востребованными: Struts и Ruby on Rails. Эти среды разработки наметили пути развития для сотен рабочих сред, созданных позже.
Классические MVC фреймворки:
Фреймворк Django ввел новую терминологию MTV.
В Django функции, отвечающие за обработку логики, соответствуют части Controller из MVC, но называются View, а отображение соответствует части View из MVC, но называется Template. Получилось, что:
Так появилась аббревиатура MTV.
TADA!! Django invented MTV
Вся логика при таком подходе вынесена во View, а то, как будут отображаться данные в Template. Из-за ограничений HTTP протокола, View в Django описывает, какие данные будут представленны по запросу на определенный URL. View, как и протокол HTTP, не хранит состояний и по факту является обычной функцией обратного вызова, которая запускается вновь при каждом запросе по URL. Шаблоны (Templates), в свою очередь, описывают, как данные представить пользователю.
MTV фреймворки:
В защиту своего дизайна авторы Pyramid написали довольно большой документ, который призван развеять мифы о фреймворке. Например, на критику модели MVC в Pyramid следует подробное объяснение, что MVC «притянут за уши» к веб-приложениям. Следующая цитата хорошо характеризует подход к терминологии в Pyramid:
«Мы считаем, что есть только две вещи: ресурсы (Resource) и виды (View). Дерево ресурсов представляет структуру сайта, а вид представляет ресурс.
«Шаблоны» (Template) в реальности лишь деталь реализации некоторого вида: строго говоря, они не обязательны, и вид может вернуть ответ (Response) и без них.
Нет никакого «контроллера» (Controller): его просто не существует.
«Модель» (Model) же либо представлена деревом ресурсов, либо «доменной моделью» (domain model) (например, моделью SQLAlchemy), которая вообще не является частью каркаса.
Нам кажется, что наша терминология более разумна при существующих ограничениях веб-технологий.»
Веб ограничен URL, который и представляет из себя дерево ресурсов или структуру сайта.
Также протокол HTTP не позволяет хранить состояние и отправлять/принимать оповещения клиенту от сервера, что ограничивает возможность отслеживания действий клиента для последующего уведомления модели на изменение состояния.
Поэтому данные часто используются на «frontend»-е (например в связке React/Redux), а на стороне сервера формируются только один раз во время ответа, либо загружаются отдельным запросом при помощи AJAX, или даже с помощью других протоколов, например WebSocket.
RV фреймворки:
Приведем структуру нашего блога к следующему виду:
.
├── __init__.py
├── models.py
└── views.py
0 directories, 3 files
Примечание
Исходный код доступен по адресу:
Где:
__init__.py
- входная точка программы, которая содержит основные
настройки и запуск Веб-сервераmodels.py
- код, который представляет данные, обычно называется моделиviews.py
- логика программы (в нашем случае WSGI-приложения)Предупреждение
Примеры работают только в Python3
models.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 | ARTICLES = [
{
'id': 1,
'title': 'Lorem ipsum dolor sid amet!',
'content': '''
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur vel tortor eleifend, sollicitudin nisl quis, lacinia augue.
Duis quam est, laoreet sit amet justo vitae, viverra egestas sem.
Maecenas pellentesque augue in nibh feugiat tincidunt. Nunc magna ante,
mollis vitae ultricies eu, consectetur id ante. In ut libero eleifend,
blandit ipsum a, ullamcorper nunc. Sed bibendum eget odio eget
pellentesque. Curabitur elit felis, pellentesque id feugiat et, tincidunt
ut mauris. Integer vitae vehicula nunc. Integer ullamcorper, nunc in
volutpat auctor, elit leo convallis nulla, vitae varius mi nisl ac lorem.
Sed a lacus mi. In hac habitasse platea dictumst. Cras in posuere velit,
id dignissim nisl. Interdum et malesuada fames ac ante ipsum primis in
faucibus. Nulla bibendum suscipit convallis.
'''
},
{
'id': 2,
'title': 'Hello',
'content': 'Test2'
},
{
'id': 3,
'title': 'World',
'content': 'Test2'
},
] |
Мы использовали самописные WSGI-middleware, которые решают стандартные задачи. Заменим их на уже существующие:
Настройки авторизации __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 | 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__':
from paste.httpserver import serve
app = make_wsgi_app()
serve(app, host='0.0.0.0', port=8000) |
Настройки URL-диспетчеризации __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 | 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__':
from paste.httpserver import serve
app = make_wsgi_app()
serve(app, host='0.0.0.0', port=8000) |
WSGI-приложение можно указывать как объект (BlogRead
) или как строку
импорта ("views.BlogIndex"
).
views.py
:
1 2 3 4 5 6 7 8 9 | class BaseArticle(BaseBlog):
def __init__(self, *args):
super(BaseArticle, self).__init__(*args)
article_id = self.environ['wsgiorg.routing_args'][1]['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None)) |
urlrelay добавляет результат поиска в переменную с названием wsgiorg.routing_args
.
Практически не изменились.
views.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 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 | from models import ARTICLES
class BaseBlog(object):
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
class BaseArticle(BaseBlog):
def __init__(self, *args):
super(BaseArticle, self).__init__(*args)
article_id = self.environ['wsgiorg.routing_args'][1]['id']
(self.index,
self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
if art['id'] == int(article_id)),
(None, None))
class BlogIndex(BaseBlog):
def __iter__(self):
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1>Simple Blog</h1>'
yield b'<a href="/article/add">Add article</a>'
yield b'<br />'
yield b'<br />'
for article in ARTICLES:
yield str.encode(
'''
{0} - (<a href="/article/{0}/delete">delete</a> |
<a href="/article/{0}/edit">edit</a>)
<a href="/article/{0}">{1}</a><br />
'''.format(
article['id'],
article['title']
)
)
class BlogCreate(BaseBlog):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
max_id = max([art['id'] for art in ARTICLES])
ARTICLES.append(
{'id': max_id+1,
'title': values[b'title'].pop().decode(),
'content': values[b'content'].pop().decode()
}
)
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> CREATE</h1>'
yield b'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title"><br>
Content:<br>
<textarea name="content"></textarea><br><br>
<input type="submit" value="Submit">
</form>
'''
class BlogRead(BaseArticle):
def __iter__(self):
if not self.article:
self.start('404 Not Found', [('content-type', 'text/plain')])
yield b'not found'
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> READ</h1>'
yield str.encode(
'<h2>{}</h2>'.format(self.article['title'])
)
yield str.encode(self.article['content'])
class BlogUpdate(BaseArticle):
def __iter__(self):
if self.environ['REQUEST_METHOD'].upper() == 'POST':
from urllib.parse import parse_qs
values = parse_qs(self.environ['wsgi.input'].read())
self.article['title'] = values[b'title'].pop().decode()
self.article['content'] = values[b'content'].pop().decode()
self.start('302 Found',
[('Content-Type', 'text/html'),
('Location', '/')])
return
self.start('200 OK', [('Content-Type', 'text/html')])
yield b'<h1><a href="/">Simple Blog</a> -> UPDATE</h1>'
yield str.encode(
'''
<form action="" method="POST">
Title:<br>
<input type="text" name="title" value="{0}"><br>
Content:<br>
<textarea name="content">{1}</textarea><br><br>
<input type="submit" value="Submit">
</form>
'''.format(
self.article['title'],
self.article['content']
)
)
class BlogDelete(BaseArticle):
def __iter__(self):
self.start('302 Found', # '301 Moved Permanently',
[('Content-Type', 'text/html'),
('Location', '/')])
ARTICLES.pop(self.index)
yield b'' |