Chapter 12: Компоненты и плагины

Компоненты и плагины

component
plugin

Компоненты и плагины являются относительно новыми особенностями web2py, и есть некоторые разногласия между разработчиками о том, что они из себя представляют и какими они должны быть. Большая часть путаницы проистекает из различных способов использования этих терминов в других программных проектах и из того, что разработчики все еще работают над завершением спецификации.

Тем не менее, поддержка плагинов является важной особенностью, и нам необходимо предоставить некоторые определения. Эти определения не являются окончательными, а просто соответствуют шаблонам программирования, которые мы хотим обсудить в этой главе.

Мы постараемся решить две проблемы здесь:

  • Как мы построить модульные приложения, которые минимизируют нагрузку на сервер с максимальным повторным использованием кода?
  • Как распространять куски кода в более или менее манере "установил плагин и играй"?

Компоненты это решения первой проблемы; плагины это решения второй.

Компоненты, LOAD и Ajax

load
LOAD
Ajax

Компонент является функционально-автономной частью веб-страницы.

Компонент может состоять из модулей, контроллеров и представлений, но нет строгого требования, за исключением, когда встроенный в веб-страницы компонент должен быть локализован внутри HTML тега (например DIV, SPAN, или IFRAME) и он должен выполнять свою задачу независимо от остальной части страницы. Мы специально заинтересованы в компонентах, которые загружаются на странице и обмениваются данными с помощью функции контроллера компонента через Ajax.

Примером компонента является "компонент комментарий", который содержится в DIV и показывает комментарии пользователей и публикует форму для добавления нового комментария. Когда форма будет отправлена, она отправляется на сервер через Ajax, список обновляется, и комментарий сохраняется на стороне сервера в базе данных. Содержание DIV обновляется без перезагрузки остальной части страницы.

Загрузка LOAD

Функция web2py LOAD позволяет с легкостью выполнить загрузку без явного знания JavaScript/Ajax или программирования.

Наша цель состоит в том, чтобы иметь возможность разработки веб-приложений путем сборки компонентов в макетах страниц.

Рассмотрим простое web2py приложение "test", которое расширяет скаффолдинг-приложение по умолчанию с пользовательской моделью в файле "models/db_comments.py":

db.define_table('comment_post',
   Field('body','text',label='Your comment'),
   auth.signature)

одним действием в "controllers/comments.py"

@auth.requires_login()
def post():
    return dict(form=SQLFORM(db.comment_post).process(),
                comments=db(db.comment_post).select())

и соответствующим представлением "views/comments/post.html"

{{extend 'layout.html'}}
{{for post in comments:}}
<div class="post">
  On {{=post.created_on}} {{=post.created_by.first_name}}
  says <span class="post_body">{{=post.body}}</span>
</div>
{{pass}}
{{=form}}

Вы можете получить доступ к нему, как обычно:

http://127.0.0.1:8000/test/comments/post

До сих пор нет ничего особенного в этом действии, но мы можем превратить его в компонент путем определения нового представления с расширением ".load", который не расширяет макет.

Поэтому мы создаем "views/comments/post.load":

{{for post in comments:}}
<div class="post">
  On {{=post.created_on}} {{=post.created_by.first_name}}
  says <blockquote class="post_body">{{=post.body}}</blockquote>
</div>
{{pass}}
{{=form}}

Мы можем получить доступ к нему по URL

http://127.0.0.1:8000/test/comments/post.load

Это компонент, который мы можем встроить в любую другую страницу, просто сделав:

{{=LOAD('comments','post.load',ajax=True)}}

Например, в "controllers/default.py" мы можем отредактировать

def index():
    return dict()

и в соответствующем представлении добавить компонент:

{{extend 'layout.html'}}
{{=LOAD('comments','post.load',ajax=True)}}

При посещении страницы

http://127.0.0.1:8000/test/default/index

покажет нормальное содержание и компонент комментариев:

image

Компонент {{=LOAD(...)}} отображает следующее:

<script type="text/javascript"><!--
web2py_component("/test/comment/post.load","c282718984176")
//--></script><div id="c282718984176">loading...</div>

(фактический сгенерированный код зависит от параметров, переданных функции LOAD).

Функция web2py_component(url,id) определена в "web2py_ajax.html" и она выполняет все волшебство: она вызывает url с помощью Ajax и встраивает ответ в DIV с соответствующими id; она улавливает каждую отправку формы в DIV и передает эти формы с помощью Ajax. Целью Ajax всегда является собственно DIV.

LOAD подпись

Полная подпись помощника LOAD состоит в следующем:

LOAD(c=None, f='index', args=[], vars={},
     extension=None, target=None,
     ajax=False, ajax_trap=False,
     url=None,user_signature=False,
     timeout=None, times=1,
     content='loading...',**attr):

Здесь:

  • первые два аргумента c и f являются соответственно контроллером и функцией, которую мы хотим вызвать.
  • args и vars аргументы и переменные, которые мы хотим передать к функции. Первый является списком, последний является словарем.
  • extension является необязательным расширением. Обратите внимание на то, что расширение также может быть передано как часть функции, как в f='index.load'.
  • target является id целевого DIV. Если данный аргумент не был указан, то генерируется случайный целевой id.
  • ajax должен быть установлен в True если DIV должен быть заполнен с помощью Ajax и False если DIV должен быть заполнен перед возвратом текущей страницы (что позволит избежать вызова Ajax). Если установить в False, то код компонента и представления будет выполнен в той же самой web2py среде, что и вызывающий объект.
  • ajax_trap=True означает, что любая форма представления в DIV должна быть перехвачена и отправлена через Ajax, а ответ должен быть отображен внутри DIV. ajax_trap=False указывает на то, что формы должны быть направлены нормально с перезагрузкой всей страницы. ajax_trap игнорируется и считается равным True если ajax=True.
  • url, если он указан, отменяет значения c, f, args, vars и extension и загружает компонент указанному поurl. Он используется для загрузки в качестве компонентов страниц, подаваемых другими приложениями (которые могут или не могут быть созданы с web2py). Обратите внимание, что использование url подразумевает ajax всегда равным True, потому что web2py не может знать заранее находится ли компонент в пределах web2py или это просто внешняя страница.
  • user_signature по умолчанию используется значение False, но если вы вошли в систему, то он должен быть установлен в значение True. Это позволит убедиться, что обратный вызов Ajax имеет цифровую подпись. Это описано в главе 4.
  • times определяет, сколько раз компонент должен быть запрошен. Использование "infinity" поддерживает беспрерывную загрузку компонента. Эта опция полезна при переключении регулярного распорядка для запроса данного документа.
  • timeout устанавливает время ожидания в миллисекундах перед началом запроса или частоту если times больше 1.
  • content представляет собой содержание, которое будет отображаться во время выполнения вызова Ajax. Это может быть помощником, как в content=IMG(..).
  • необязательный **attr (атрибуты) могут быть переданы во вложенный DIV.

Если не указано представление с .load, то существует generic.load, которое отображает словарь, возвращенный действием без макета. Это работает лучше всего, если словарь содержит один элемент.

Если ваш компонент LOAD имеет расширение .load и соответствующая функция контроллера перенаправляет к другому действию (например, форму входа), то расширение .load распространяется и на новый URL-адрес (и на другие перенаправления тоже), который также загружаются с расширением .load.

Перенаправление из компонента

Для перенаправления из компонента, используйте это:

redirect(URL(...),client_side=True)

Но обратите внимание, что перенаправленные URL будут по умолчанию иметь расширение компонента. Смотрите примечания по аргументу extension функции URL в Главе 4

Перезагрузка страницы через перенаправление после представления компонента

Если вы вызываете действие с помощью Ajax и вы хотите, чтобы действие принудительно перенаправило родительскую страницу, то вы можете сделать это с помощью перенаправления из функции LOAD контроллера. Если вы хотите перезагрузить родительскую страницу, то вы можете перенаправить на него. Родительский URL известен (смотрите Клиент-серверный компонент коммуникации)

поэтому после обработки отправки формы, функция контроллера перезагружает родительскую страницу через перенаправление:

if form.process().accepted: 
    ...
    redirect( request.env.http_web2py_component_location,client_side=True)

Обратите внимание, что в разделе ниже, Клиент-серверный компонент коммуникации, описывается, как компонент может возвращать JavaScript, который может быть использован для более сложных действий, когда компонент представлен. Конкретный случай перезагрузки другого компонента описан далее.

Перезагрузка другого компонента

При использовании нескольких компонентов на странице, вы можете поручить одному компоненту перезагрузить другой. Вы можете сделать это путем выдачи поручения компоненту вернуть некий JavaScript.

Можно жестко прописать целевой DIV, но в этом рецепте мы используем переменную строки запроса, чтобы сообщить подавшему заявку контроллеру, какой компонент мы хотим перезагрузить. Он идентифицируется через id блока DIV, содержащего целевой компонент. В этом случае, DIV имеет id равный 'map'. Обратите внимание, что необходимо использовать target='map' внутри цели LOAD; без этого, целевой идентификатор будет выбран случайным образом и метод reload() не будет работать. Смотрите подпись LOAD выше.

В представлении, сделайте это:

{{=LOAD('default','submitting_component.load',ajax=True,vars={'reload_div':'map'})}}

Контроллеру, принадлежащему к предоставленному компоненту, необходимо отправить обратно JavaScript, так что просто добавьте в существующий код контроллера при обработке отправки:

if form.process().accepted:
...
    if request.vars.reload_div:
        response.js =  "jQuery('#%s').get(0).reload()" % request.vars.reload_div

(Ко всему прочему, удалите перенаправление, если вы использовали подход предыдущего раздела.)

Вот именно. JavaScript библиотеки web2py смотрятся после перезагрузки. Это может быть обобщено для обработки нескольких компонентов с Javascript, что выглядит как:

jQuery('#div1,#div2,#div3').get(0).reload()

Для получения дополнительной информации о response.js смотрите Клиент-серверный компонент коммуникации (ниже).

Ajax post не поддерживает многоэлементные формы

Поскольку Ajax post не поддерживает многоэлементные формы, т.е. загрузка файлов, поля загрузки не будет работать с компонентом LOAD. Вы можете обмануть себя думая, что это будет работать, потому что поля загрузки будут нормально функционировать если POST выполняется из индивидуального компонента в .load представлении. Вместо этого, загрузки выполняются с помощью AJAX-совместимых сторонних виджетов и web2py вручную загруженных команд хранения.

LOAD и Клиент-серверный компонент коммуникации

Когда действие компонента вызывается через Ajax, web2py передает два HTTP-заголовка вместе с запросом:

web2py-component-location
web2py-component-element

которые могут быть доступны действию через переменные:

request.env.http_web2py_component_location
request.env.http_web2py_component_element

Последний также доступен через:

request.cid

request.cid

Первый содержит URL-адрес страницы, который вызывается действием компонента. Последний содержит id компонента DIV, который будет содержать ответ.

Действие компонента может также хранить данные в двух специальных заголовках ответа HTTP, которые будут интерпретированы на всю страницу после ответа. Ими являются:

web2py-component-flash
web2py-component-command

и они могут быть установлены через:

response.headers['web2py-component-flash']='....'
response.headers['web2py-component-command']='...'

или (если действие вызывается через компонент) автоматически через:

response.flash='...'
response.js='...'

Первый содержит текст, который вы хотите высветить после ответа. Последний содержит JavaScript код, который вы хотите выполнить после ответа. Он не может содержать символы новой строки.

В качестве примера, давайте определим форму contact компонента в "controllers/contact/ask.py" что позволяет пользователю задать вопрос. Компонент будет отправлять по электронной почте вопрос к системному администратору, высвечивать сообщение "thank you" и удалять компонент со страницы:

def ask():
    form=SQLFORM.factory(
        Field('your_email',requires=IS_EMAIL()),
        Field('question',requires=IS_NOT_EMPTY()))
    if form.process().accepted:
        if mail.send(to='admin@example.com',
                  subject='from %s' % form.vars.your_email,
                  message = form.vars.question):
            response.flash = 'Thank you'
            response.js = "jQuery('#%s').hide()" % request.cid
        else:
            form.errors.your_email = "Unable to send the email"
    return dict(form=form)

Первые четыре строки определяют форму и принимают ее. Почтовый объект, используемый для отправки определяется в скаффолдинг-приложении по умолчанию. Последние четыре строки реализуют всю логику конкретного компонента посредством получения данных из заголовков HTTP запроса и настройки заголовков HTTP ответа.

Теперь вы можете внедрить эту контактную форму на любой странице через

{{=LOAD('contact','ask.load',ajax=True)}}

Обратите внимание на то, что мы не определяем .load представление для нашего ask компонента. Нам это не нужно, потому что он возвращает один объект (форму) и, следовательно, "generic.load" сделает это просто прекрасно. Помните, что общие представления являются средством разработки. В производстве вы должны скопировать "views/generic.load" в "views/contact/ask.load".

user_signature
requires_signature
Мы можем блокировать доступ к функции, вызываемой через Ajax, посредством цифровой подписи URL, используя аргумент user_signature:

{{=LOAD('contact','ask.load',ajax=True,user_signature=True)}}

который добавить цифровую подпись к URL. Цифровая подпись должна быть затем проверена с помощью декоратора в функции обратного вызова:

@auth.requires_signature()
def ask(): ...

Захваченные Ajax ссылки и помощник A

A
Ajax links

Как правило, ссылка не захватывается, и нажав на ссылку внутри компонента, вся связанная страница загружается. Иногда вам необходимо, чтобы связанная страница была загружена внутри компонента. Это может быть достигнуто с помощью A помощника:

{{=A('linked page',_href='http://example.com',cid=request.cid)}}

Если cid указан, то связанная страница загружается через Ajax. cid является id HTML-элемента, где размещается загруженный контент страницы. В этом случае мы устанавливаем его в request.cid, т.е id компонента, который генерирует ссылку. Связанная страница может быть и, как правило, является внутренним URL, генерируемым с использованием URL помощника .

Плагины

A плагин является любым подмножеством файлов приложения.

и мы действительно имели в виду любое:

  • Плагин не является модулем, не является моделью, это и не контроллер и не представление, но он может содержать модули, модели, контроллеры и/или представления.
  • Плагин не должен быть функционально автономным и может зависеть от других плагинов или конкретного пользовательского кода.
  • Плагин не является системой плагинов и, следовательно, не имеет ни малейшего понятия о регистрации, ни об изоляции, хотя мы дадим правила, чтобы попытаться достичь некоторой изоляции.
  • Мы говорим о плагине для вашего приложения, а не о плагине для web2py.

Так почему же он называется плагин? Потому что он обеспечивает механизм для упаковки подмножества приложения и распаковки его на другом приложении (т.е. вставной (plug-in)). Согласно этому определению, любой файл в приложении можно рассматривать как плагин.

Когда приложение распространяется, то его плагины упакованы и распространяются вместе с ним.

На практике, admin предоставляет интерфейс для упаковки и распаковки плагинов отдельно от вашего приложения. Файлы и папки вашего приложения, которые имеют имена с префиксом plugin_имя могут быть упакованы вместе в файл с именем:

web2py.plugin.имя.w2p

и распространяться вместе.

image

Любой файл может быть частью плагина и эти файлы не обрабатываются web2py иначе, чем другие файлы. Кроме того, файлы и папки, которые имеют префикс plugin_ распознаются в admin и группируются вместе в admin в соответствии с их постфиксом наименования. admin относится к ним по-другому, не как web2py.

image

На практике мы будем иметь дело только с двумя типами плагинов:

  • Плагины компонента. Эти плагины, которые содержат компоненты, как определено в предыдущем разделе. Плагин компонента может содержать один или несколько компонентов. Мы можем рассмотреть, например предложенный выше plugin_comments, который содержит компонент comments. Другим примером может быть plugin_tagging, который содержит компонент tagging и компонент tag-cloud, который опубликовывает некоторые таблицы базы данных, также подходит под определение плагина.
  • Плагины макета. Это плагины, которые содержат макет представления и статические файлы, необходимые для данного макета. Когда плагин применяется, он придает приложению новый внешний вид и ощущения.

В приведенных выше определениях, компоненты, созданные в предыдущем разделе, например, "controllers/contact.py", уже являются плагинами. Мы можем перемещать их из одного приложения к другому и использовать компоненты, которые они определяют. Тем не менее, они не признаются в качестве таковых в интерфейсе admin, потому что нет ничего, что помечает их как плагины. Таким образом, существуют две проблемы, которые мы должны решить:

  • Именовать файлы плагина, используя соглашение, с тем чтобы admin смог признать их принадлежность к одному и тому же плагину.
  • Если плагин имеет файлы модели, то необходимо разработать соглашение, с тем чтобы объекты, которые он определяет не загрязняли пространство имен и не вступали в противоречие друг с другом.

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

Правило 1:

Модели и контроллеры плагина должны называться, соответственно

  • models/plugin_name.py
  • controllers/plugin_name.py

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

  • views/plugin_name/
  • modules/plugin_name/
  • static/plugin_name/
  • private/plugin_name/

Правило 2:

В моделях плагина можно определить только те объекты, имена которых начинаются с

  • plugin_name
  • PluginName
  • _

Правило 3:

В моделях плагина можно определить только те переменные сессии, имена которых начинаются с

  • session.plugin_name
  • session.PluginName

Правило 4:

Плагины должны включать в себя лицензию и документацию. Они должны быть помещены в:

  • static/plugin_name/license.html
  • static/plugin_name/about.html

Правило 5:

Плагин может полагаться только на существование глобальных объектов, определенных в скаффолдинге "db.py", т.е.

  • подключение к базе данных под названием db
  • Экземпляр Auth под названием auth
  • Экземпляр Crud под названием crud
  • Экземпляр Service под названием service

Некоторые плагины могут быть более сложными и иметь параметр конфигурации в случае существования более одного экземпляра db.

Правило 6:

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

PluginManager

Следуя приведенным выше правилам, мы можем быть убеждены, что:

  • admin признает все plugin_name файлы и папки как часть единого целого.
  • плагины не мешают друг другу.

Правила выше, не решают проблему версий плагинов и зависимостей. Это выходит за наши рамки.

Плагины компонента

component plugin

Плагины компонента являются плагинами, которые определяют компоненты. Компонентам, как правило, доступна база данных и определения своих собственных моделей.

Здесь мы вернемся к предыдущему компоненту comments внутри comments_plugin используя тот же самый код, который мы писали ранее, но придерживаясь всех предыдущих правил.

Во-первых, мы создаем модель под названием "models/plugin_comments.py":

db.define_table('plugin_comments_comment',
   Field('body','text', label='Your comment'),
   auth.signature)

def plugin_comments():
    return LOAD('plugin_comments','post',ajax=True)

(заметим, что последние две строки определяют функцию, которая упрощает внедрение плагина)

Во-вторых, мы определим "controllers/plugin_comments.py"

def post():
    if not auth.user:
        return A('login to comment',_href=URL('default','user/login'))
    comment = db.plugin_comments_comment
    return dict(form=SQLFORM(comment).process(),
                comments=db(comment).select())

В-третьих, мы создаем представление под названием "views/plugin_comments/post.load":

{{for comment in comments:}}
<div class="comment">
  on {{=comment.created_on}} {{=comment.created_by.first_name}}
  says <span class="comment_body">{{=comment.body}}</span>
</div>
{{pass}}
{{=form}}

Теперь мы можем использовать admin чтобы упаковать плагин для распределения. Администратор сохранит этот плагин как:

web2py.plugin.comments.w2p

Мы можем использовать плагин в любом представлении, просто установив плагин через страницу Правка в admin и добавив в наши собственные представления

{{=plugin_comments()}}

Конечно, мы можем сделать плагин более сложным при наличии компонентов, которые принимают параметры и параметры конфигурации. Чем сложнее компоненты, тем труднее становится избегать конфликта имён. Менеджер плагинов, описанный ниже, позволяет избежать этой проблемы.

Менеджер плагинов

PluginManager является классом, определенном вgluon.tools. Перед тем, как объяснить, как он работает изнутри, мы объясним, как использовать его.

Здесь мы рассмотрим предыдущий plugin_comments и сделаем его лучше. Мы хотим иметь возможность индивидуальной настройки:

db.plugin_comments_comment.body.label

без необходимости редактирования собственного кода плагина.

Вот как мы можем это сделать:

Во-первых, перепишем плагин "models/plugin_comments.py" следующим образом:

def _():
    from gluon.tools import PluginManager
    plugins = PluginManager('comments', body_label='Your comment')

    db.define_table('plugin_comments_comment',
        Field('body','text',label=plugins.comments.body_label),
        auth.signature)
    return lambda: LOAD('plugin_comments','post.load',ajax=True)
plugin_comments = _()

Обратите внимание на то, как весь код, за исключением определения таблицы инкапсулируется в одной функции, называемой _ с тем чтобы он не загрязнял глобальное пространство имен. Также обратите внимание, как функция создает экземпляр PluginManager.

Теперь в любой другой модели в вашем приложении, например, в "models/db.py", Вы можете настроить этот плагин следующим образом:

from gluon.tools import PluginManager
plugins = PluginManager()
plugins.comments.body_label = T('Post a comment')

Объект plugins уже создал экземпляр в скаффолдинг приложении по умолчанию "models/db.py"

Объект PluginManager является одиночным объектом Storage уровня потока из объектов Storage. Это означает, что в одном приложении вы можете создать столько экземпляров, сколько вам нравится, но (неважно имеют ли они тоже самое имя или нет) они действуют так, как если бы они были одним экземпляром PluginManager.

В частности, каждый файл плагина может сделать свой собственный объект PluginManager и зарегистрировать себя и свои параметры по умолчанию с ним:

plugins = PluginManager('name', param1='value', param2='value')

Вы можете изменить эти параметры в другом месте (например, в "models/db.py") с кодом:

plugins = PluginManager()
plugins.name.param1 = 'other value'

Можно настроить несколько плагинов в одном месте.

plugins = PluginManager()
plugins.name.param1 = '...'
plugins.name.param2 = '...'
plugins.name1.param3 = '...'
plugins.name2.param4 = '...'
plugins.name3.param5 = '...'

Когда плагин определен, PluginManager должен принимать аргументы: имя плагина и дополнительные именованные аргументы, которые являются параметрами по умолчанию. Тем не менее, когда плагины сконфигурированы, конструктор PluginManager не должен принимать никаких аргументов. Конфигурация должна предшествовать определению плагина (т.е. она должна быть в файле модели, который приходит первым по алфавиту).

Плагины макета

layout plugin

Плагины макета проще, чем плагины компонента, потому что они, как правило, не содержат код, а только представления и статические файлы. Тем не менее, вы все равно должны следовать хорошей практике:

Во-первых, создайте папку с именем "static/plugin_layout_name/" (где имя это имя вашего макета) и поместите все ваши статические файлы там.

Во-вторых, создайте файл макета с именем "views/plugin_layout_name/layout.html" который содержит ваш макет и ссылки на изображения, CSS и файлы JavaScript в "static/plugin_layout_name/"

В-третьих, модифицируйте "views/layout.html" так, чтобы он просто читался:

{{extend 'plugin_layout_name/layout.html'}}
{{include}}

Преимущество этой конструкции состоит в том, что пользователям этого плагина можно установить несколько макетов и выбрать, какой из них применять путем простого редактирования "views/layout.html". Кроме того, "views/layout.html" не будет упакован admin вместе с плагином, так что нет никакого риска, что плагин будет перекрывать код пользователя в ранее установленном макете.

Репозиторий плагина, установка плагина через администратора

В то время как не существует единого репозитория web2py плагинов вы можете найти многие из них на одном из следующих URL-адресов:

http://web2pyslices.com (это ведущий репозиторий и интегрируется в приложение администратора web2py всего одним нажатием кнопки установки)

http://web2py.com/plugins
http://web2py.com/layouts

Последние версии web2py администратора позволяют автоматически получать и устанавливать плагины от web2pyslices. Добавление плагина к приложению, редактирование его через приложение администратора, и выбор Загрузки Плагинов, доступно в настоящее время в нижней части экрана.

Чтобы опубликовать свои собственные плагины, создайте учетную запись на web2pyslices.

Вот скриншот, показывающий некоторые из авто-устанавливаемых плагинов:

image

 top