Chapter 12: Componentes e plugins
Componentes e plugins
Componentes e plugins são recursos relativamente novos do web2py, e há algum desacordo entre os desenvolvedores sobre o que eles são e o que deveriam ser. A maior parte da confusão decorre dos diferentes usos desses termos em outros projetos de software e do fato de que os desenvolvedores ainda estão trabalhando para finalizar as especificações.
No entanto, o suporte a plugins é um recurso importante e precisamos fornecer algumas definições. Essas definições não pretendem ser definitivas, apenas coerentes com os padrões de programação que queremos discutir neste capítulo.
Vamos tentar resolver dois problemas aqui:
Como podemos construir aplicativos modulares que minimizam a carga do servidor e maximizam a reutilização de código? Como podemos distribuir pedaços de código de uma forma mais ou menos plug-and-play?
Componentes abordam a primeira questão; plugins endereçam o segundo.
Componentes, LOAD e Ajax
Um componente é uma parte funcionalmente autônoma de uma página da web.
Um componente pode ser composto de módulos, controladores e visualizações, mas não há nenhum requisito estrito além de, quando incorporado em uma página da Web, estar localizado em uma tag html (por exemplo, um DIV, um SPAN ou um IFRAME) e ele deve executar sua tarefa independentemente do resto da página. Estamos especificamente interessados em componentes que são carregados na página e se comunicam com a função do controlador de componente via Ajax.
Um exemplo de um componente é um "componente de comentários" que está contido em um DIV e mostra comentários de usuários e um formulário de comentários pós-novos. Quando o formulário é enviado, ele é enviado ao servidor via Ajax, a lista é atualizada e o comentário é armazenado no banco de dados. O conteúdo DIV é atualizado sem recarregar o restante da página.
LOAD
A função LOAD web2py torna isso fácil de ser feito sem conhecimento ou programação explícita de JavaScript/Ajax.
Nosso objetivo é ser capaz de desenvolver aplicativos web, reunindo componentes em layouts de página.
Considere um simples aplicativo web2py "test" que estende o aplicativo padrão de scaffolding com um modelo personalizado no arquivo "models/db_comments.py":
db.define_table('comment_post',
Field('body', 'text', label='Your comment'),
auth.signature)
uma ação em "controllers/comments.py"
@auth.requires_login()
def post():
return dict(form=SQLFORM(db.comment_post).process(),
comments=db(db.comment_post).select())
e as "visualizações/comentários/post.html" correspondentes
{{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}}
Você pode acessá-lo como de costume em:
http://127.0.0.1:8000/test/comments/post
Até agora não há nada especial nesta ação, mas podemos transformá-lo em um componente definindo uma nova visão com a extensão ".load" que não estende o layout.
Por isso criamos um "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}}
Nós podemos acessá-lo no URL
http://127.0.0.1:8000/test/comments/post.load
Este é um componente que podemos incorporar em qualquer outra página simplesmente fazendo
{{=LOAD('comments', 'post.load', ajax=True)}}
Por exemplo, em "controllers/default.py" podemos editar
def index():
return dict()
e na vista correspondente, adicione o componente:
{{extend 'layout.html'}}
{{=LOAD('comments', 'post.load', ajax=True)}}
Visitando a página
http://127.0.0.1:8000/test/default/index
mostrará o conteúdo normal e o componente de comentários:
o {{=LOAD(...)}}
componente é processado da seguinte forma:
<script type="text/javascript"><!--
web2py_component("/test/comment/post.load", "c282718984176")
//--></script><div id="c282718984176">loading...</div>
(o código gerado de fato depende das opções passadas para a função LOAD).
o web2py_component(url, id)
função é definida em "web2py_ajax.html" e executa toda a magia: chama o url
via Ajax e incorpora a resposta para o DIV com correspondente id
; Ele captura todos os formulários enviados para o DIV e envia esses formulários via Ajax. O alvo do Ajax é sempre o próprio DIV.
Assinatura LOAD
A assinatura completa do auxiliar LOAD é a seguinte:
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):
Aqui:
- os dois primeiros argumentos
c
ef
são o controlador e a função que queremos chamar respectivamente. args
evars
são os argumentos e variáveis que queremos passar para a função. O primeiro é uma lista, o último é um dicionário.extension
é uma extensão opcional. Observe que a extensão também pode ser passada como parte da função como emf='index.load'
.target
é oid
do alvo DIV. Se não for especificado um alvo aleatórioid
é gerado.ajax
deve ser definido paraTrue
se o DIV tiver que ser preenchido via Ajax eFalse
se o DIV tiver que ser preenchido antes que a página atual seja retornada (evitando assim a chamada do Ajax). Se definido paraFalse
, o código e a visão do componente serão executados no mesmo ambiente web2py do chamador.ajax_trap=True
significa que qualquer envio de formulário no DIV deve ser capturado e enviado via Ajax, e a resposta deve ser renderizada dentro do DIV.ajax_trap=False
indica que os formulários devem ser enviados normalmente, recarregando a página inteira.ajax_trap
é ignorado e assumido como sendoTrue
E seajax=True
.url
, se especificado, substitui os valores dec
,f
,args
,vars
eextension
e carrega o componente nourl
. Ele é usado para carregar como páginas de componentes servidas por outros aplicativos (que podem ou não ser criados com o web2py). Note que usandourl
assumeajax
ComoTrue
sempre, porque web2py não pode saber de antemão se o componente é processado dentro de web2py ou é apenas uma página externauser_signature
O padrão é Falso, mas, se você estiver logado, deve ser definido como Verdadeiro. Isso garantirá que o retorno de chamada do ajax seja assinado digitalmente. Isso está documentado no capítulo 4.times
especifica quantas vezes o componente deve ser solicitado. Use "infinito" para continuar carregando o componente continuamente. Essa opção é útil para acionar rotinas regulares para uma determinada solicitação de documento.timeout
define o tempo de espera em milissegundos antes de iniciar o pedido ou a freqüência setimes
é maior que 1.content
é o conteúdo a ser exibido durante a execução da chamada do ajax. Pode ser um ajudante como emcontent=IMG(..)
.- opcional
**attr
(atributos) podem ser passados para o conteúdoDIV
.
Se não .load
vista é especificado, há uma generic.load
que processa o dicionário retornado pela ação sem layout. Funciona melhor se o dicionário contiver um único item.
Se você LOAD um componente com o .load
extensão e a função de controlador correspondente redireciona para outra ação (por exemplo, um formulário de login), o .load
extensão se propaga eo novo url (aquele a redirecionar também) também é carregado com um .load
extensão.
Redirecionar de um componente
Para redirecionar de um componente, use isto:
redirect(URL(...), client_side=True)
mas observe que o URL redirecionado será o padrão para a extensão do componente. Veja notas sobre o argumento extension
dentro the URL function in Capítulo 4
Recarregar página por meio de redirecionamento após o envio do componente
Se você chamar uma ação via Ajax e quiser que a ação force um redirecionamento da página pai, você pode fazer isso com um redirecionamento da função do controlador LOADed. Se você quiser recarregar a página pai, você pode redirecionar para ela. O URL pai é conhecido Client-Server component communications )
então, depois de processar o envio do formulário, a função do controlador recarrega a página pai por meio do redirecionamento:
if form.process().accepted:
...
redirect(request.env.http_web2py_component_location, client_side=True)
Note que a seção abaixo, Client-Server component communications , descreve como o componente pode retornar o javascript, que pode ser usado para ações mais sofisticadas quando o componente é submetido. O caso específico de recarregar outro componente é descrito a seguir.
Recarregar outro componente
Se você usar vários componentes em uma página, talvez queira enviar um componente para recarregar outro. Você faz isso obtendo o componente enviado para retornar algum javascript.
É possível codificar o DIV alvo, mas nessa receita usamos uma variável de string de consulta para informar ao controlador de envio qual componente queremos recarregar. É identificado pelo id do DIV que contém o componente de destino. Neste caso, o DIV possui o id 'map'. Note que é necessário usar target='map'
na CARGA do alvo; sem isso, o ID de destino é randomizado e o recarregamento () não funciona. Veja a assinatura LOAD acima.
Na visão, faça isso:
{{=LOAD('default', 'submitting_component.load', ajax=True, vars={'reload_div':'map'})}}
O controlador pertencente ao componente de envio precisa enviar o javascript de volta, portanto, adicione isso ao código do controlador existente ao processar o envio:
if form.process().accepted:
...
if request.vars.reload_div:
response.js = "jQuery('#%s').get(0).reload()" % request.vars.reload_div
(Claro, remova o redirecionamento se você estivesse usando a abordagem da seção anterior). É isso aí. As bibliotecas javascript do web2py cuidam do recarregamento. Isso poderia ser generalizado para lidar com vários componentes com javascript parecido com:
jQuery('#div1,#div2,#div3').get(0).reload()
Para obter mais informações sobre response.js, consulte Client-Server component communications (abaixo).
A postagem do Ajax não suporta formulários multipartes
Como a postagem do Ajax não suporta formulários com diversas partes, ou seja, uploads de arquivos, os campos de upload não funcionarão com o componente LOAD. Você pode se enganar pensando que funcionaria porque os campos de upload funcionarão normalmente se o POST for feito a partir da visualização .load do componente individual. Em vez disso, os uploads são feitos com widgets de terceiros compatíveis com ajax e comandos de armazenamento de upload manual do web2py.
Comunicações de componente LOAD e Client-Server
Quando a ação de um componente é chamada via Ajax, o web2py passa dois cabeçalhos HTTP com o pedido:
web2py-component-location
web2py-component-element
que pode ser acessado pela ação através das variáveis:
request.env.http_web2py_component_location
request.env.http_web2py_component_element
Este último também é acessível via:
request.cid
O primeiro contém o URL da página que chamou a ação do componente. Este último contém o id
do DIV que conterá a resposta.
A ação do componente também pode armazenar dados em dois cabeçalhos de resposta HTTP especiais que serão interpretados pela página inteira na resposta. Eles são:
web2py-component-flash
web2py-component-command
e eles podem ser definidos via:
response.headers['web2py-component-flash']='....'
response.headers['web2py-component-command']='...'
ou (se a ação for chamada por um componente) automaticamente via:
response.flash='...'
response.js='...'
O primeiro contém o texto que você deseja que seja exibido em resposta. Este último contém o código JavaScript que você deseja executar na resposta. Não pode conter novas linhas.
Como exemplo, vamos definir um componente de formulário de contato em "controllers/contact/ask.py" que permite ao usuário fazer uma pergunta. O componente enviará a pergunta por e-mail ao administrador do sistema, exibirá uma mensagem de "agradecimento" e removerá o componente da página:
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='[email protected]',
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)
As primeiras quatro linhas definem o formulário e aceitam-no. O objeto de correio usado para envio é definido no aplicativo de andaimes padrão. As últimas quatro linhas implementam toda a lógica específica do componente obtendo dados dos cabeçalhos de solicitação HTTP e definindo os cabeçalhos de resposta HTTP.
Agora você pode inserir este formulário de contato em qualquer página
{{=LOAD('contact', 'ask.load', ajax=True)}}
Observe que não definimos um .load
vista para o nosso ask
componente. Nós não temos que, porque ele retorna um único objeto (formulário) e, portanto, o "generic.load" vai fazer muito bem. Lembre-se de que as visualizações genéricas são uma ferramenta de desenvolvimento. Na produção, você deve copiar "views/generic.load" em "views/contact/ask.load".
user_signature
argumento:{{=LOAD('contact', 'ask.load', ajax=True, user_signature=True)}}
que adicionam uma assinatura digital ao URL. A assinatura digital deve então ser validada usando um decorador na função de retorno de chamada:
@auth.requires_signature()
def ask(): ...
Links Ajax Presos e o Ajudante A
Normalmente, um link não é interceptado e, ao clicar em um link dentro de um componente, toda a página vinculada é carregada. Às vezes você quer que a página vinculada seja carregada dentro do componente. Isto pode ser conseguido usando o A
ajudante:
{{=A('linked page', _href='http://example.com', cid=request.cid)}}
E se cid
é especificado, a página vinculada é carregada via Ajax. o cid
é o id
do elemento html onde colocar o conteúdo da página carregada. Neste caso, configuramos para request.cid
, ou seja, o id
do componente que gera o link. A página vinculada pode ser e geralmente é uma URL interna gerada usando o URL helper .
Plugins
Um plugin é qualquer subconjunto dos arquivos de um aplicativo.
e nós realmente queremos dizer "qualquer":
- Um plugin não é um módulo, não é um modelo, não é um controlador, não é uma visão, mas pode conter módulos, modelos, controladores e/ou visualizações.
- Um plugin não precisa ser funcionalmente autônomo e pode depender de outros plugins ou código de usuário específico.
- Um plugin não é um sistema de plugins e portanto não tem nenhum conceito de registro nem isolamento, embora nós damos regras para tentar obter algum isolamento.
- Estamos falando de um plug-in para seu aplicativo, não de um plug-in para web2py.
Então, por que é chamado de plugin ? Porque ele fornece um mecanismo para empacotar um subconjunto de um aplicativo e desempacotá-lo em outro aplicativo (por exemplo, plug-in ). De acordo com essa definição, qualquer arquivo no seu aplicativo pode ser tratado como um plug-in.
Quando o aplicativo é distribuído, seus plugins são empacotados e distribuídos com ele.
Na prática, o admin fornece uma interface para empacotar e descompactar plugins separadamente do seu aplicativo. Arquivos e pastas do seu aplicativo que possuem nomes com o prefixo plugin_
name pode ser agrupado em um arquivo chamado:
web2py.plugin.
nome .w2p
e distribuídos juntos.
Qualquer arquivo pode ser parte de um plugin e esses arquivos não são tratados pelo web2py de forma diferente de outros arquivos. Exceto que arquivos e pastas em que têm um plugin_
prefixo são reconhecidos por admin e garoupa juntos por admin de acordo com o nome do postfix. admin trata-os de forma diferente, não web2py.
Na prática, nos preocuparemos apenas com dois tipos de plugins:
- Plugins de componentes . Estes são plugins que contêm componentes, conforme definido na seção anterior. Um plug-in de componente pode conter um ou mais componentes. Podemos pensar, por exemplo, em um
plugin_comments
que contém o componente comentários proposto acima. Outro exemplo poderia serplugin_tagging
que contém um componente tagging e um componente tag-cloud que compartilham algumas tabelas do banco de dados também definidas pelo plugin. - Plugins de Layout . Estes são plugins que contêm uma visualização de layout e os arquivos estáticos requeridos por tal layout. Quando o plug-in é aplicado, ele dá ao aplicativo uma nova aparência.
Pelas definições acima, os componentes criados na seção anterior, por exemplo "controllers/contact.py", já são plugins. Podemos movê-los de um aplicativo para outro e usar os componentes que eles definem. No entanto, eles não são reconhecidos como tal por admin porque não há nada que os rotule como plugins. Então, há dois problemas que precisamos resolver:
- Nomeie os arquivos do plug-in usando uma convenção, para que admin pode reconhecê-los como pertencentes ao mesmo plugin
- Se o plug-in tiver arquivos de modelo, estabeleça uma convenção para que os objetos definidos não poluam o espaço de nomes e não entrem em conflito entre si.
Vamos supor que um plugin seja chamado de name . Aqui estão as regras que devem ser seguidas:
Regra 1: Modelos e controladores de plugins devem ser chamados, respectivamente
models/plugin_
nome.py
controllers/plugin_
nome.py
e exibições de plug-in, módulos, arquivos estáticos e privados devem estar em pastas chamadas, respectivamente:
views/plugin_
nome/
modules/plugin_
nome/
static/plugin_
nome/
private/plugin_
nome/
Regra 2: Os modelos de plug-in só podem definir objetos com nomes que começam com
plugin_
nomePlugin
Nome_
Regra 3: Os modelos de plug-in só podem definir variáveis de sessão com nomes que começam com
session.plugin_
nomesession.Plugin
Nome
Regra 4: Os plugins devem incluir licença e documentação. Estes devem ser colocados em:
static/plugin_
nome/license.html
static/plugin_
nome/about.html
Regra 5: O plugin só pode confiar na existência dos objetos globais definidos no scaffolding "db.py", ou seja,
- uma conexão de banco de dados chamada
db
- a
Auth
instância chamadaauth
- uma
Crud
instância chamadacrud
- uma
Service
instância chamadaservice
Alguns plugins podem ser mais sofisticados e ter um parâmetro de configuração no caso de existir mais de uma instância de db.
Regra 6: Se um plugin precisar de parâmetros de configuração, estes devem ser configurados através de um PluginManager, conforme descrito abaixo.
Seguindo as regras acima, podemos garantir que:
- admin reconhece todo o
plugin_
'nome' 'arquivos e pastas como parte de uma única entidade. - plugins não interferem uns com os outros.
As regras acima não resolvem o problema das versões e dependências do plugin. Isso está além do nosso escopo.
Plugins de componentes
Plugins de componentes são plugins que definem componentes. Componentes geralmente acessam o banco de dados e definem com seus próprios modelos.
Aqui nos voltamos a anterior comments
componente em um comments_plugin
usando o mesmo código que escrevemos antes, mas seguindo todas as regras anteriores.
Primeiro, criamos um modelo chamado "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)
(note que as duas últimas linhas definem uma função que simplificará a incorporação do plugin)
Em segundo lugar, definimos um "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())
Em terceiro lugar, criamos uma visualização chamada "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}}
Agora podemos usar admin para embalar o plugin para distribuição. Admin irá salvar este plugin como:
web2py.plugin.comments.w2p
Podemos usar o plug-in em qualquer visualização simplesmente instalando o plugin através do editar página em admin e adicionando isso às nossas próprias visões
{{=plugin_comments()}}
É claro que podemos tornar o plugin mais sofisticado, tendo componentes que levam parâmetros e opções de configuração. Quanto mais complexos os componentes, mais difícil se torna evitar colisões de nomes. O gerenciador de plug-ins descrito abaixo foi projetado para evitar esse problema.
Gerenciador de plugins
o PluginManager
é uma classe definida em gluon.tools
. Antes de explicar como funciona dentro, vamos explicar como usá-lo.
Aqui nós consideramos o anterior plugin_comments
e nós fazemos melhor. Queremos poder personalizar:
db.plugin_comments_comment.body.label
sem ter que editar o próprio código do plugin.
Aqui está como podemos fazer isso:
Primeiro, reescreva o plugin "models/plugin_comments.py" desta forma:
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 = _()
Observe como todo o código, exceto a definição da tabela, é encapsulado em uma única função chamada _
para que não polua o namespace global. Observe também como a função cria uma instância de um PluginManager
.
Agora, em qualquer outro modelo em seu aplicativo, por exemplo, em "models/db.py", você pode configurar este plug-in da seguinte forma:
from gluon.tools import PluginManager
plugins = PluginManager()
plugins.comments.body_label = T('Post a comment')
o
plugins
objeto já está instanciado no aplicativo padrão de scaffolding em "models/db.py"
O objeto PluginManager é um objeto de armazenamento singleton de nível de thread de objetos Storage. Isso significa que você pode instanciar quantos você quiser dentro do mesmo aplicativo, mas (se eles têm o mesmo nome ou não) eles agem como se houvesse uma única instância do PluginManager.
Em particular, cada arquivo de plug-in pode criar seu próprio objeto PluginManager e registrar-se e seus parâmetros padrão com ele:
plugins = PluginManager('name', param1='value', param2='value')
Você pode sobrescrever estes parâmetros em outro lugar (por exemplo, em "models/db.py") com o código:
plugins = PluginManager()
plugins.name.param1 = 'other value'
Você pode configurar vários plugins em um só lugar.
plugins = PluginManager()
plugins.name.param1 = '...'
plugins.name.param2 = '...'
plugins.name1.param3 = '...'
plugins.name2.param4 = '...'
plugins.name3.param5 = '...'
Quando o plug-in é definido, o PluginManager deve receber argumentos: o nome do plug-in e os argumentos nomeados opcionais, que são os parâmetros padrão. No entanto, quando os plugins são configurados, o construtor PluginManager não deve receber argumentos. A configuração deve preceder a definição do plug-in (isto é, deve estar em um arquivo de modelo que vem primeiro em ordem alfabética).
Plugins de layout
Os plug-ins de layout são mais simples que os plug-ins de componente porque geralmente não contêm código, mas apenas exibições e arquivos estáticos. No entanto, você ainda deve seguir uma boa prática:
Primeiro, crie uma pasta chamada "static/plugin_layout _ name /" (onde name é o nome do seu layout) e coloque todos os seus arquivos estáticos lá.
Segundo, crie um arquivo de layout chamado "views/plugin_layout _ name /layout.html" que contenha seu layout e vincule as imagens, os arquivos CSS e JavaScript em "static/plugin_layout _ name /"
Terceiro, modifique o "views/layout.html" para que ele simplesmente leia:
{{extend 'plugin_layout_name/layout.html'}}
{{include}}
O benefício deste design é que os usuários deste plugin podem instalar vários layouts e escolher qual deles aplicar simplesmente editando "views/layout.html". Além disso, "views/layout.html" não será embalado por admin juntamente com o plug-in, não há risco de que o plug-in substitua o código do usuário no layout instalado anteriormente.
Repositórios de plugins, plugins de instalação via admin
Embora não haja um único repositório de plug-ins web2py, você pode encontrar muitos deles em um dos seguintes URLs:
http://web2pyslices.com (this is the leading repository and is integrated to the web2py admin application for one-click installs)
http://web2py.com/plugins
http://web2py.com/layouts
Versões recentes do admin do web2py permitem a busca e a instalação automáticas de plugins a partir de web2pyslices. Para adicionar um plug-in a um aplicativo, edite-o por meio do aplicativo de administração e escolha Download de plug-ins, atualmente na parte inferior da tela.
Para publicar seus próprios plugins, crie uma conta em web2pyslices.
Aqui está uma captura de tela mostrando alguns dos plugins auto-instaláveis: