Python: Веб-разработка без фреймворков
Эта серия статей была написана в 2008 году и с тех пор успела несколько устареть. Изначально в планах был еще ряд статей в развитие темы, но потом я передумал.
Self-coded RSS A blog on Python, WebOb, programming technique and miscellaneous projects by Sergey Schetinin.
Python: Веб-разработка без фреймворковЭта серия статей была написана в 2008 году и с тех пор успела несколько устареть. Изначально в планах был еще ряд статей в развитие темы, но потом я передумал. Часть 1: ОсновыКаждый разработчик тщательно выбирает свой инструментарий и чем лучше он им владеет, тем эффективнее его работа и тем более востребованы его услуги. В то же время многие программисты гордятся своей ленью и ищут такие инструменты, которые делали бы за них как можно больше работы. При этом, на мой взгляд, забывают, что бесплатный сыр только в мышеловке и гоняются за иллюзорным «лучшим» фреймворком тщательно сверяя какой из них делает больше работы: Zope, ROR или Django? Мой опыт говорит о том, что время на изучение фреймворков и подстройка под их ограничения почти никогда не окупается, а пользуясь минимальным инструментарием, с которым я хочу вас ознакомить, можно добиться гораздо лучших результатов. WSGIНе так давно библиотек и фреймворков для веб-разработки на Python было очень много, и взаимодействовали они между собой не наилучшим образом. Выбрав для разработки одно решение приходилось придерживаться его и в дальнейшем. Разрешением этой проблемы многие считали выбрать один-два главных фреймворка и сконцентрироваться на их разработке, таким образом сделав остальные ненужными. Такая консолидация решила бы проблемы с взаимодействием разных частей, но и минусов у неё порядочно. Ведь так много библиотек возникло не зря, и причина этого именно в том что нет очевидно предпочтительного подхода к веб-разработке. Решение подоспело в виде стандарта WSGI (PEP-333). Это сверх-компактная спецификация, которая решает как вопросы взаимодействия разных компонент веб-приложений, так и связки между этими приложениями и HTTP-сервером. Вкратце, WSGI (Web Server Gateway Interface) требует от приложения предоставлять следующий интерфейс:
Т.е. приложение будет вызвано с двумя аргументами. Первый — словарь environ, содержащий различные данные о запросе, второй — функция запускающая процесс ответа на запрос. В основном содержимое environ совпадает с содержимым переменных окружения CGI скрипта при аналогичном запросе, откуда и происходит имя этого аргументa, это же позволяет использовать уже имеющиеся средства для разбора строки запроса итп. Назначение start_response проще показать на примере, а возвращать WSGI приложение должно итератор по строкам, т.е. функция может быть генератором и «скармливать» веб-серверу ответ по частям. Например:
Удобство использования WSGI заключается, конечно, не в том, что мы можем писать такой код, а в том, что предоставив такой интерфейс, мы получаем доступ к множеству совместимых библиотек. Например, мы элементарно можем запустить сервер с вышеуказанным приложением:
Мы можем также предоставить его через FastCGI, SCGI или еще какой-нибудь диковинный протокол общения между HTTP-сервером и сервером приложений. Существует также mod_wsgi, модуль Apache, позволяющий в считанные минуты запустить ваше приложение через этот сервер, причем с отличной интеграцией с его инфраструктурой. Ну и, конечно же, мы можем вызывать другие WSGI приложения и обрабатывать их результаты (о чем будет сказано в разговоре о middleware). Этот стандарт к тому же делает возможным библиотеки тестирования веб-приложений, не привязанные ни к каким фреймворкам. В результате библиотек для веб-разработки врядли стало меньше, ведь создавать новые теперь куда проще, но проблема совместимости была решена. Теперь любой фреймворк, вне зависимости от того каким образом он сообщается с вашим кодом, предоставляет WSGI. Различные приложения, не использующие сторонних фреймворков, также доступны через WSGI (например Trac). PythonPastePythonPaste (paste — тесто) это замечательное собрание различной функциональности реализованной для WSGI и общей для разных фреймворков. Если вы заглянете своей любимой библиотеке под капот и не найдете там Paste, то выбор ваш возможно не так уж хорош. Впрочем, сам Paste настолько удобен, что ознакомившись с его возможностями, не должно возникнуть желания использовать какой-либо фреймворк вообще. Например, в нем есть готовая поддержка самых разных способов аутентификации (Basic, Digest, form, signed cookie, auth_tkt), поддержка корректной и удобной генерации ответов и заголовков (к примеру редиректы, Cache-control, Expires, gzipper и прочие). Различные базовые средства комбинации приложений (URLMap, Cascade, Recursive), статических данных (с учетом Etag, If-Modified итп). Скажем так:
Как видите, кода написано минимум, а часть сайта уже защищена паролем и соответствует разным папкам на диске в зависимости от имени пользователя. Представить себе, что нечто подобное можно записать короче и понятней, трудно. Можно также предоставлять статические файлы прямо из архива (ArchiveStore), есть поддержка сессий, возможность отслеживать прогресс загрузки на сервер больших форм, и прочее и прочее. PythonPaste для отладкиОсобо радуют средства для отладки приложений. Тут и возможность приложения автоматически перезапускаться при изменении части исходных кодов, и профайлер для каждого запроса, и возможность слать отчеты об ошибках на почту (или сохранять их локально). Можно валидировать все [X]HTML ответы сервера, есть те самые средства тестирования WSGI приложений итд итп. Есть даже возможность интерактивной отладки прямо в браузере. На этом стоило бы остановиться особо, но это лучше один раз увидеть, чем сто раз прочитать, просто запустите этот код и откройте в браузере http://localhost:8080/. (Также можно посмотреть скринкаст).
Часть 2: WebObВ прошлой части я постарался рассказать о том, что чистый WSGI код писать не так уж сложно и что преимущества такого подхода налицо, но есть ли у этого обратная сторона? Единственным, пожалуй, недостатком я могу назвать некоторые неудобства по работе с данными в запросе. Был ли запрос GET или POST? Какая у запроса кодировка? Неужели значения формы надо разбирать при помощи webob.RequestВ библиотеке WebOb есть класс Request, позволяющий работать с данными из environ с куда большим комфортом. Строки с различными данными из HTTP запроса и от обработавшего его сервера превращаются в удобные в использовании, богатые на функциональность объекты. Доступ к cookies ( Очень удобный доступ к пути, по которому было найдено данное WSGI приложение ( Если вы хотите поддерживать HTTP стандарт по максимуму, то вам повезло. Атрибут if_modified_since — экземпляр datetime, а обработка значения из заголовка Accept позволяет без хлопот выбрать предпочтительный для клиента формат ответа ( ПримерПриведу небольшой пример использования этого класса. Представим, что перед нами стоит задача написать форум и мы хотим, чтобы у приложения были красивые URLы и не менее красивое разделение функциональности внутри. Нередко для решения такой задачи пытаются применить какие-то средства фреймворка, но, обычно, оказывается, что инструмент вроде и подходит, но по большому счету не годится и надо делать всё равно самому. Бывает, пользуются какой-нибудь специальной библиотекой (как например Routes). Нередко умудряются связаться с регулярными выражениями и файлами конфигурации лежащими в специальной папке. А ведь ничего сложного нет:
Если такого фрагмента нет, то мы выведем список имеющихся на форуме тем (переложив эту задачу на соответствующее приложение), если этот фрагмент Обратите внимание на фрагмент с показом отдельной темы — мы не передаем номер темы как аргумент, а сохраняем его в запросе, таким образом мы не требуем от view_topic_app нарушения WSGI стандарта и сохраняем возможность показывать темы и по другим запросам, нужно только помнить заранее добавлять в запрос данные о идентификаторе темы. Другой немаловажный момент это использование ДекораторЧитатель может справедливо заметить, что код получился не самым чистым из возможных. Это верно, но можно привести всё в порядок при помощи небольшого декоратора. Декоратор впоследствии будет использоваться повсеместно, я бы даже ратовал за включение его в дистрибутив WebOb, но там уже есть нечто похожее, но как по мне куда более неприглядное. Как мы видим, в forum_app функция start_response непосредственно не вызывается, она только передается далее по цепочке. По большому счету результаты за forum_app генерируют другие приложения (кроме 404, но и на это управа найдется), поэтому давайте согласимся, что принимать нашему приложению положено будет объект Request, а возвращать WSGI приложение. Подчеркиваю, не вызывать а именно возвращать, вызов выполнит уже декоратор. Это можно реализовать вот так:
Код приложения теперь стал хорошо причесанным:
Обратите внимание на HTTPNotFound, такие же приложения есть для всех возможных HTTP ответов:
Возвращать 404 приходится довольно часто, так что стоит поменять в декораторе пару строк:
Благодаря этому оборачиваемая функция может сообщить что ничего не было найдено просто вернув webob.exc.HTTPExceptionМодуль webob.exc содержит реализацию HTTP ответов со всевозможными статус-кодами. Удобной особенностью является то, что все эти реализации наследуют от класса HTTPException и при наличии в стеке HTTPExceptionMiddleware из того же модуля, можно делать (Кстати, этот модуль практически точная копия модуля На мой же взгляд разумно добавить поддержку таких исключений прямо в наш декоратор, благо это увеличит его лишь на три строки, судите сами:
Если хотите попробовать развить этот проект самостоятельно, вот наш полный код на данный момент, включая заглушки для недостающих частей. Рекомендую прочитать еще раз и убедиться как всё компактно и понятно:
Часть 3: ОтветыПрочитав предыдущие статьи читатель, надеюсь, убедился, что средств PythonPaste и WebOb более чем достаточно для разбора запроса, композиции приложений и выполнения множества стандартных задач. Далее мы будем рассматривать вопросы генерации ответов, развертывания на сервере и выбора различных вспомогательных библиотек и инструментов. До сих пор в примерах мы почти не занимались собственно ответами, перекладывая эту задачу на какие-то уже существующие приложения. Как уже было упомянуто в описании стандарта WSGI, ответы можно генерировать по частям, однако, как правило, сначала приложение выполняет все нужные вычисления, затем подставляет результаты в шаблон и возвращает полученный документ. Для такой модели работы с ответами хорошо подходит класс webob.Response. Впрочем, он также имеет поддержку последовательной генерации (см. app_iter), просто о ней мы говорить не будем. webob.ResponseПервое, что нужно понять, это то, что экземпляры этого класса являются WSGI приложениями. Создав и наполнив такой экземпляр данными мы получаем WSGI приложение, которое можно использовать единожды или многократно, по желанию. Давайте разберем его использование на примере (внимание, используется декоратор из прошлой статьи:
Хотелось бы сказать пару теплых слов о поддержке браузерами Unicode в запросах: ни IE, ни столь горячо любимый всеми Firefox не способны указать в какой кодировке были отправлены данные формы (если ваш браузер с этой задачей справляется, напишите, пожалуйста, в комментариях). Поэтому, чтобы получать данные формы как положено, в Unicode, нам пришлось добавить несколько строк, которые устанавливают кодировку запроса по умолчанию в UTF-8. Место этих строк, конечно, в нашем незабвенном декораторе, поэтому давайте туда их и перенесем. В результате тело hello_app укоротилось до четырех строк:
Как видно из примера, ответ можно и даже следует создавать из Unicode строк — HTTP ответ будет сгенерирован корректно. Также видно, что экземпляры Response «многоразовые» (см. выше: form_app). Постепенное наполнение ответа даннымиResponse не обязательно наполнять данными при инициализации. Можно делать это постепенно, например вот так:
В целом это понятно из кода, но на всякий случай обращу внимание, что мы теперь используем cookies для запоминания имени пользователя и опять-таки все значения Unicode, как и должно быть. Совсем несложно контролировать кеширование (.cache_expires) и условные ответы. Делать это самостоятельно весьма трудоемко, и мало какой фреймворк предоставляет для этого средства. Итак, добавив лишь пару строк, мы получаем поддержку Etag / If-None-Match:
Тут вызов md5_etag() вычисляет Etag как MD5 хеш тела ответа, но при желании etag можно устанавливать самостоятельно. Ничуть не сложнее работать с If-Modified-Since, для этого у ответа устанавливается .last_modified. Таким образом, хоть мы и сгенерировали ответ целиком, он будет передан клиенту только в том случае, если его кеш устарел. Request.get_responseКак правило, мы получаем экземпляры Request и строим экземпляры Response, но иногда нам понадобится делать всё ровно наоборот, а именно для тестирования и для написания middleware. Создать новый экземпляр Request не имея полного WSGI окружения очень просто:
Естественно, можно наполнить запрос данными присваивая значения соответствующим атрибутам. Использовать же такой объект можно вовсе не только с теми функциями и методами которые готовы работать с WebOb. На самом деле мы можем вызвать с его помощью любое WSGI приложение и получить результат в удобном нам виде, а именно Response:
Весьма удобно, что
Как уже упоминалось, такой подход работает для любых WSGI приложений:
Часть 4: ДеплойментВ предыдущих статьях мы разобрались, как можно создавать веб-приложения на Python используя лишь необходимые средства. Следующим этапом будет развертывание приложения на сервере и связанная с этим задача конфигурации его компонент (deployment). Сама задача WSGI стандарта — установить интерфейс, через который HTTP сервер будет общаться с веб-приложением, так что не приходится беспокоиться поддерживается ли нравящийся нам вариант связки или сервер. Выбор большой:
В зависимости от вашего выбора связка выполняется или просто или очень просто, так что выбирать следует исходя из того в каком окружении будет работать ваше приложение и с каким сервером вы наиболее знакомы. Я перепробовал много вариантов, но когда появился mod_wsgi опробовал и перешел на его использование во всех случаях когда только возможно. mod_wsgiКак ясно из названия, это модуль Apache написанный специально для связки непосредственно с WSGI приложениями. Я думаю для многих важно, что в нем есть базовая поддержка хостинга пользовательских скриптов в shared-hosting окружениях. На самом деле одной из ведущих мотиваций при написании этого замечательного модуля было сделать хостинг веб-приложений на Python доступнее, а сами приложения в таких условиях быстрее. mod_wsgi показал себя отлично в реальных условиях, работает стабильно, быстро и безошибочно, потому можно рассчитывать, что он получит распространение среди хостеров. Также он на пути к включению в основные репозитории Debian. Приблизительная модель работы этого модуля такова:
Каким же именно образом в конфигурации сервера указывается WSGI приложения и их конфигурация? Пожалуй самым простым из возможных — указанием Python-скрипта по исполнению которого в его пространстве имен окажется переменная application значение которой и будет использовано как WSGI-приложение. Скрипт может иметь любое имя, но принято давать таким файлам расширение .wsgi. Как правило, такой скрипт будет импортировать все необходимые компоненты и конфигурировать их для данного размещения. Может показаться, что это какой-то недостаточно мощный или непродуманный подход, но опыт показывает, что это оптимальное решение. Для того чтобы стало понятней почему я так считаю, давайте кратко рассмотрим альтернативный подход. PasteDeployPasteDeploy — это, в первую очередь, средство конфигурации и компоновки WSGI приложений, а также выбора и конфигурации связки с веб-сервером. Предназначено оно, среди прочего, для конечного пользователя / администратора не обязательно знакомого с Python, поэтому конфигурация хранится в .ini файлах. Эту конфигурацию можно запускать, мониторить и в целом использовать как UNIX-демон благодаря команде paster из PasteScript. На первый взгляд это хорошее решение, но практическое его использование раскрывает ряд недостатков. Я не буду вдаваться в подробности его применения, так как моя цель раскритиковать такой подход, а не научить им пользоваться. Вкратце конфигурация содержит отдельные секции для каждого используемого приложения, фильтра (middleware) и сервера. Используемые приложения указываются в виде URI в специальных схемах. Наиболее используемая URI-схема egg: указывает на setuptools entry point, который является заводом (factory) по производству искомых приложений из передаваемой конфигурации. Также есть схема для использования других конфигурационных файлов (config:). Я не буду перечислять преимущества системы, сосредоточившись на недостатках, потому что они не так очевидны без опыта использования.
Хотя часть этих проблем может быть решена конфигурацией в другом виде, например XML, большая часть проблем неизбежна при использовании отдельного языка для конфигурации приложений. Если же для конфигурации использовать Python-скрипты, то все упомянутые проблемы решаются сами собой — повторное использование параметризованных блоков конфигурации превращается ни во что иное как определение и вызовы функций. Не нужны никакие уловки для построения более сложных структур данных. Использование точек входа становится необязательным. И именно так работают wsgi-скрипты для mod_wsgi. Такое решение, кажущееся на первый взгляд кустарным, оказывается гораздо более мощным и удобным в использовании, чем любая возможная альтернатива. Мы теряем возможности PasteScript по управлению демонами сервера приложений, но это нас не беспокоит, так как теперь они управляются совместно с Apache, для их перезапуска применяется привычное Что до конфигурации приложений непрограммистами, то править файл с синтаксисом Python ничуть не сложнее чем аналогичный INI. Запуск WSGI-скриптов без ApacheЧто ж, с развертыванием на сервере более-менее ясно, а как быть с разработкой? Будем поднимать локальный сервер, настраивать его, перезапускать всякий раз когда вносим в код изменения? Я считаю что не стоит, и потому привожу свой модуль для исполнения wsgi-скриптов (это сокращенный, но рабочий вариант):
Сохраним этот файл как run_wsgiscript.py там, где он будет доступен импортированию. Лучше оформить его как пакет, но самым простым вариантом будет разместить его в site-packages. Такой скрипт загружает WSGI приложение образом аналогичным mod_wsgi, но в дополнение он оборачивает его парой отладочных middleware и запускает HTTP-сервер (по умолчанию на 8080-м порту). Также, благодаря paste.reloader, раз в секунду будет проверяться наличие изменений в самом скрипте и загруженных им модулях. Если такие изменения появятся, то скрипт выйдет с кодом ошибки 3; подразумевается, что внешний, вызывающий его скрипт воспринимает это как сигнал к перезапуску, о чем чуть дальше. Если при загрузке скрипта произошла ошибка (обычно это происходит если во внесенных изменениях есть синтаксическая ошибка), то на консоль будет выведен трейсбек и наш модуль выдержит пятисекундную паузу перед перезапуском (чтобы дать спокойно прочитать где была ошибка). Последняя недостающая часть нашей системы — это внешний скрипт который перезапукает наш модуль. Я разрабатываю под Windows и пользуюсь таким (файл run-wsgi.bat):
Если всё еще сохранилась неясность почему и как это сделано, просто посмотрите краткую документацию paste.reloader — мы просто применили эту систему к загрузке wsgi скриптов. SciTEТем, кто, как и я, использует в работе SciTE, будет удобно добавить в его конфигурацию следующие строки, которые добавят wsgi -скриптам корректную подсветку и позволят запускать их по F5:
WSGI-скриптНа всякий случай, упреждая возможные вопросы, приведу пример скрипта. Поскольку wsgi-скрипт это полноценный питоновский файл, то мы можем определить WSGI приложение прямо в нем. Т.е. любой из примеров приведенных в предыдущих статьях можно использовать таким образом. Только теперь там, где мы вызывали paste.httpserver.serve(APP) нужно писать application = APP. Например вот наш самый первый пример в виде WSGI-скрипта (добавилась последняя строка кода):
В Apache тоже ничего мудрить не надо, для начала достаточно такого:
Не думаю что статья отвечает на все вопросы, но моей задачей было, в первую очередь, показать что в этом вопросе нет ничего страшного и отослать к документации тех компонент которые могут пригодиться. Часть 5: MiddlewareВ первых четырех статьях этой серии мы успели рассмотреть значительную часть инструментария, который нам понадобится для написания веб-приложений. Начиная с этой статьи я постараюсь показать как это всё выглядит на практике — как структурируется код, как совмещаются компоненты и т. п. Это не столько инструкция к действию, сколько демонстрация того, что нет необходимости в поддержке со стороны фреймворка. По мере усложнения вашего проекта код конечно будет меняться, но в каждый момент он будет лучше соответствовать имеющейся задаче чем какие-то заготовленные решения. Я думаю трудно поспорить с тем, что модульность кода это хорошо, ведь независимость компонент позволяет более полно тестировать каждую из них, облегчает разработку в команде, совершенно очевидно, что рефакторить меньшими блоками гораздо удобнее и т.д. Но писать компонентный код нужно еще уметь. MiddlewareСтандарт какого-то интерфейса создает пространство для особого рода компонент — middleware. Это слово часто употребляют в смысле «адаптер» (совместно с которыми мидлварь часто используется), хотя это не совсем верно. Я стараюсь использовать его только в узком смысле — компонента предоставляющая и потребляющая один и тот же интерфейс. Например, load-balancer это middleware, кеширующая прослойка — тоже, а вот преобразователь XML-RPC ↔ SOAP уже под вопросом. В нашем случае middleware будет потреблять и предоставлять, конечно же, WSGI. Таким образом у нас есть возможность придать новые свойства любому существующему WSGI-приложению. Ряд уже готовых mw уже был упомянут в первой статье серии, и их еще огромное множество, но мы рассмотрим, как написать такую компоненту с нуля. Справедливость ради, стоит упомянуть, что некоторые фреймворки также используют middleware. Например, существует Django middleware, которое, естественно, работает только в своей песочнице и потому для всех остальных бесполезно. JSMinНачнем c примера попроще. Всем кто использовал в приложениях JavaScript известно, что, для ускорения загрузки, скрипты можно «минифицировать». Для этого есть разные инструменты, так что само по себе это не проблема. Неудобство в том, что иметь отдельные укороченные скрипты очень неудобно — чтобы их содержимое не устаревало нужно добавить специальный шаг в процесс деплоймента, да и может выйти, что какой-то из скриптов был забыт и прочие подобные неприятности. Для большинства случаев можно обойтись меньшей кровью написав мидлварь которая паковала бы скрипты на лету. На машине разработчика использовать её не стоит т.к. может понадобиться полистать используемые скрипты в браузере, но если вы усвоили идеи из статьи про деплоймент, то ничего сложного в этом не окажется. Для упаковки скриптов есть готовый модуль, поэтому задача состоит лишь в том, чтобы превратить его в мидлварь. Информации из предыдущих статей более чем достаточно, для того чтобы решить эту задачу, поэтому, прежде чем читать далее, попробуйте решить её самостоятельно. Решением должна быть такая функция
РешениеРешение может быть например таким:
Тут нет ничего нового. Мы просто создали замыкание (middleware_app), которое является WSGI-приложением, которое, вместо того чтобы генерировать ответ самостоятельно, поручает эту задачу обернутому приложению. Затем полученный от него ответ мы проверяем по типу содержимого и если там JS, то обрабатываем тело ответа jsmin. Обращу внимание на ряд вещей:
Ошибки в реализацииНо в этом решении есть как минимум две ошибки, ведь мы не учли некоторых возможностей HTTP. Попробуйте самостоятельно понять, в чем они заключаются. Ошибки такие: мы не учитываем, что ответ может быть закодированным ( Content-EncodingЧтобы исправить первое, достаточно добавить r.decode_content(), впрочем, поскольку скрипты неплохо бы еще и сжать при передаче, давайте добавим и это.
Как вариант последние две строки можно записать так:
Эти варианты не идентичны. В том случае если user agent прислал заголовок
Таким образом мы указываем что отдавать скрипты в сжатом виде для нас предпочтительнее в пять раз. В таком случае на тот же запрос данные всё же будут запакованы (так как. Безусловно, в данном случае, это всё изыски имеющие мало общего с реально стоящими задачами, но случаи бывают разные и возможность работать с HTTP-стандартом как положено порой оказывается очень ценной. RangeИсправить ошибку с частичным ответом ( Можно добавить условие Поэтому самый надежный способ — удалить Решение навернякаДля таких случаев также предусмотрен метод Домашнее заданиеВ следующей статье мы будем развивать затронутые здесь идеи, а пока что пара задачек для читателей. Минификация и gzip архивация занимают время, развейте имеющийся код так, чтобы обработанные ответы кешировались, в том числе пакованые gzip. Укажите какие сделаны предположения и для оборачивания каких приложений это не подойдет. Тут есть целый ряд альтернативных подходов и правильных ответов тоже множество. Создайте декоратор для удобства написания простых middleware, предполагаемое использование такое:
Часть 6: Компонетные решенияВ прошлой статье мы выяснили как может выглядеть независимая компонента, а в этой мы создадим ещу одну, чуть крупнее, и найдем способ связать её с остальным приложением. Постановка задачиДопустим мы пишем веб-приложение, использующее какую-то JS-библиотеку, в нашем примере YUI. Давайте попробуем инкапсулировать логику её подключения к странице. Для таких задач есть целый ряд «решений» вроде ToscaWidgets, но толку от них ноль, как это и принято среди всего имеющего в названии слово «widget». Фактически YUI может быть размещена на серверах Yahoo, на нашем сервере где-то рядом с приложением или где-то еще, мы будем поддерживать все эти варианты. Также удобно иметь возможность раздавать библиотеку прямо из дистрибутива (yui_x.x.x.zip) не распаковывая. Приложение будет запрашивать у компоненты HTML код для подключения интересующих её модулей. У модулей есть debug и min версии, поэтому компонента должна учитывать и это. Мы будем местами срезать углы, например, у некоторых компонент в некоторых версиях есть дополнительный суффикс ’beta’, но мы это будем игнорировать. Учесть это несложно, но в рамках статьи не оправдано. Точно также мы не будем выстраивать правильный порядок включения скриптов, зависимости, подключение CSS файлов их минификацию и прочие детали. В настоящей компоненте это всё следует реализовать — времени это займет минимум, а использовать её станет еще удобней и приятней. И всё же некоторые несущественные детали мы будем учитывать в нашей реализации для того чтобы было видно что это не требует никакой магии — всё отлично решается «в лоб». Подготовительные шагиДля начала давайте напишем небольшое приложение для тестирования компоненты:
Конечно, для целей тестирования разумно вызывать непосредственно компоненту, но этот пример заодно показывает как она используется в реальном приложении. Для компоновки не требуется никаких трюков или специальных библиотек. На самом деле интересно, кому впервые пришла идея, что так писать недостаточно хорошо, и сколько человеко-часов было угроблено на хитросплетения под девизом «не повторяй себя»? Итак, если мы захотим посмотреть в браузере как выглядит блок ссылок на скрипты history, animation и json мы откроем В результате мы увидим результат подобный следующему:
В конечном счете эта строка результат вызова
Вариант для Yahoo CDNНачнем с реализации YuiYahooHosted:
Трудно придумать что-то более очевидное. Понять что делает компонента очень просто, и это здорово даже если никто кроме вас никогда не будет читать ваш код. Но даже для простого кода документация не помешает.
Большая часть этого текста потом будет перенесена в документацию общего интерфейса или суперкласса и именно там от неё будет больше всего толку — когда реализаций несколько, важно понимать что между ними общего и чем они отличаются, для таких целей код не может заменить документации. Рефакторинг и выделение общей функциональностиЛегко заметить, что основная часть кода может быть использована для других реализаций, поэтому вынесем её в новый класс:
Это не только базовый класс, он также может быть использован непосредственно, но самое интересное конечно будет в подклассах. Для начала посмотрим какой стала реализация для размещения на Yahoo.
Что ж, пока что всё было просто, как насчет того чтобы совместить генерацию ссылок и собственно хостинг скриптов? Размещение на собственном сервереПоскольку задачи предоставления папки или zip-архива уже решены в Paste, то реализация выйдет на удивление короткой. Соотношение кода к документации, как положено, приближается к 1:1.
Опять, не прибегая ни к каким трюкам, получился качественный код. В этой реализации мы используем написанную в предыдущей статье мидлварь и вместо того чтобы для минификации добавлять суффикс —min к имени файла, мы минифицируем его самостоятельно. Это позволяет минифицировать также отладочные версии скриптов (полезно разве что для отладки скриптов удаленно размещенного приложения, что случается не часто), но главное наша минификация отрезает заголовки с копирайтом которые сохранены в файлах из дистрибутива — каждый байт на счету! А если серьезно, то собственная минификация пригодится на следующем этапе. Мы также оборачиваем приложения в gzipper, но и это можно отключить передав конструктору gzip=False. Обратите внимание на restrict_app из from_distro_zipfile, таким образом мы ограничиваем доступ папкой build из дистрибутива и облегчаем себе одну предстоящую задачу (о которой позже). Собственно в интеграции генератора ссылок и самого WSGI приложения со скриптами нет ничего мудреного, у генератора есть атрибут yuiapp с приложением, которое скрипт конфигурации должен сделать доступным по префиксу указанному в конструкторе. ИспользованиеПоскольку мы отказались от использования специальных систем конфигурации, получившийся код готов к употреблению. App — приложение использующее значение аргумента своего конструктора как генератор ссылок на скрипты. Оно делает вызовы вроде Уже размещенные скрипты:
На серверах Yahoo:
Самостоятельно:
или
Или в ходе тестирования:
Если мы знаем домен по которому будет размещен root, то стоит добавить его к первому аргументу. Обратите внимание, что однажды создав экземпляр YuiLinkGen, мы можем использовать его многократно, если у нас есть несколько приложений способных использовать такую компоненту разумно передавать им одну и ту же копию. Реализовать это не используя в конфигурации Python было бы затруднительно, к тому же не ясно: чего ради? Склеивание скриптовЕсли в прошлой статье мы сумели сделать минификацию более удобной, то возможно нам удастся упростить склейку скриптов? Чем больше запросов браузер шлет к серверу тем обычно больше задержка при загрузке страницы, поэтому при переходе в продакшн толково сделанные (читай «не встречающиеся в природе») сайты склеивают свои скрипты в один файл, все CSS-файлы в другой и используют их в таком виде. Это уменьшает количество запросов к серверу, что в свою очередь уменьшает нагрузку на него, делает проверку на изменения гораздо более быстрой (важно при обновлении страницы пользователем), gzip на склеенных скриптах эффективнее чем на раздельных итд итп. Иногда для разных страниц нужно использовать разное подмножество скриптов и тогда у подхода описанного ниже обнаружатся и недостатки, но это особый случай и решать его также нужно отдельно. Для большинства случаев мы получим заметный выигрыш используя следующую стратегию. Для начала мы сделаем middleware способную склеивать скрипты на лету. Мы хотим чтобы путь вида
Для наших нужд можно было бы опустить работу с юникодом и генерацию правильного last-modified, но я привожу эти фрагменты, чтобы не создать ложного впечатления о том насколько всё просто. Всё просто, но всё же нужно быть внимательным к деталям. По уму также можно не генерировать тело ответа целиком, а склеивать его по мере надобности в app_iter. Главным преимуществом этого была бы экономия в случае ответов 304 Not Modified, но, поскольку мы собираемся кешировать ответы, то генерация полного тела ответа — правильный подход. Генерация ссылок на склеенные скриптыТеперь нужно научить нашу реализацию генерировать ссылки на такие склеенные скрипты.
Это прямой наследник YuiLinkGen и может использоваться в тех же случаях. Например вместо блока ссылок вначале статьи он вернет
Может ли случиться что нам нужно генерировать такие ссылки не создавая соответствующего приложения для склейки? Это возможно, если такое приложение размещено на другом сервере или процессе. Для более частого случая, когда нам нужны обе части, пригодится следующий класс:
Обратите внимание на граф наследования, вернитесь к коду и прочитайте его весь еще раз. Заметьте, что YuiJoinHostedLinkGen унаследовал конструкторы from_directory и from_distro_zipfile с соответствующей им семантикой, но добавил поддержку склеивания. Я хочу еще раз подчеркнуть, что если вы пишете код в таком стиле, то вы используете или учитесь использовать те же навыки и архитектурные решения что и при разработке в любой другой области. Если вы умеете применять ООП к месту, если вы видите смысл в документировании кода, если имеете навык деления функциональности на компоненты, то это пригодится в любой области программирования. Нельзя применять особые критерии к веб-разработке: хранение запроса в глобальной переменной — в любом случае извращение, много кода ни о чем — плохой знак, если нет ясных стыков, на которых нужно писать документацию — плохи дела и т.д. Сделать хорошо — можно, но для этого нужно иметь свободу делать как угодно, и для этого WSGI бесценен. webob_middlewareВ конце прошлой статьи я предлагал написать реализацию декоратора webob_middleware (мы использовали его в этой статье) который превращал бы функцию с сигнатурой
Также см. ответ на критику (написано после 3й, опубликовано после 4й части). If my ideas are intriguing to you and you wish to subscribe to my newsletter, you can do that via RSS, Twitter, Google+ or even actual email. |