Arquivos Estáticos no Django

Resumo

De forma explorativa, este trabalho busca levantar um melhor entendimento sobre a gestão, configuração e a entrega de conteúdo estático em uma aplicação baseada no framework Django, em sua versão 1.11. Além disso busca-se fazer a distinção entre a ideia de arquivos estáticos e arquivos de mídia (media) no escopo do framework.

1. Arquivos Estáticos no Django

Diferente do que ocorre em outras tecnologias, no framework Django a ideia é explicitamente delegar a entrega de conteúdo estático para outras aplicações, tais como web servers e CDNs (Content Delivery Networks). Por padrão uma aplicação Django simplesmente não entrega conteúdo estático, o que pode causar alguma confusão, pelo menos para iniciantes na tecnologia.

Há diferentes aspectos a serem tratados dependendo do ambiente de execução da aplicação, e para entender melhor isso é necessário partir do app staticfiles, que é parte integrante do Django e pode ser ativado no projeto a qualquer momento. Com base nisso será possível discutir melhor sobre a distinção entre arquivos estáticos e arquivos de mídia e finalmente seguir ao tópico da implantação da aplicação Django em produção.

Ao interessado em mais detalhes, um ponto de partida na documentação é o seguinte link: https://docs.djangoproject.com/en/1.11/howto/static-files/.

Por fim, deve-se frisar que toda a discussão a seguir se dá com base na versão 1.11 do framework, e portanto pode ser inválida em outras versões.

2. O app staticfiles

Este é um app do Django que permite habilitar a entrega de conteúdo estático durante desenvolvimento e facilita a preparação da aplicação para implantação em produção. Para acessar suas funcionalidades é necessário primeiro ativa-lo no projeto, o que é feito ao incluir o nome de seu módulo, django.contrib.staticfiles, na configuração INSTALLED_APPS do arquivo settings.py.

A referência geral sobre o app segue em: https://docs.djangoproject.com/en/1.11/ref/contrib/staticfiles/#module-django.contrib.staticfiles

2.1. Componentes

Antes de falar sobre as funcionalidades deste app é importante entender o funcionamento dos seus principais componentes: o finder e o storage.

2.1.1. Componente Finder

A base de funcionamento do staticfiles está nos algoritmos responsáveis por encontrar os arquivos estáticos que podem estar espalhados pelo projeto. Normalmente parte destes arquivos é compartilhada por todos os apps do projeto e portanto reside em um diretório comum. Já outros arquivos podem ser específicos de cada app, e por isso permanecem isolados dentro do diretório do seu respectivo app.

Os algoritmos de busca são implementados em componentes chamados de finders, onde os principais são os seguintes: FileSystemFinder e AppDirectoriesFinder. Para ativar um ou mais finders basta incluir o nome completamente qualificado de sua classe na configuração STATICFILES_FINDERS, no arquivo settings.py, tal como ilustrado a seguir.

STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    'django.contrib.staticfiles.finders.FileSystemFinder',
]

A configuração não é obrigatória, e quando não definida assume justamente a disposição apresentada acima. Porém, mesmo que o valor padrão seja desejado, pode ser recomendável incluir a configuração explicitamente no projeto, para evitar que outros desenvolvedores tenham dificuldade de entender o funcionamento do app.

2.1.1.1. AppDirectoriesFinder

Este finder busca estabelecer um padrão, onde cada app do projeto aloca seus arquivos estáticos dentro de um diretório static. Assim, o finder procura automaticamente os arquivos de cada app configurado em INSTALLED_APPS, não sendo necessário definir o caminho de cada diretório de arquivos estáticos.

Como exemplo, segue abaixo uma estrutura onde os diretórios projeto/cliente e projeto/produto referem-se a apps devidamente ativos no projeto. Nesta configuração o AppDirectoriesFinder encontraria apenas os arquivos cliente.js e produto.js.

projeto/cliente/static/cliente.js
projeto/produto/static/produto.js
projeto/static/global.js

Notar que neste finder o nome do diretório static é uma constante e não pode ser alterado.

A classe para ativação do componente é a seguinte: django.contrib.staticfiles.finders.AppDirectoriesFinder.

2.1.1.2. FileSystemFinder

Este finder busca arquivos apenas nos diretórios que estiverem explicitamente especificados na configuração STATICFILES_DIRS do arquivo settings.py. Por exemplo, com a configuração ilustrada abaixo, a busca seria habilitada nos diretórios estaticos e produto/estaticos a partir da raiz do projeto, e também no diretório /usr/var/www/html/static a partir da raiz do sistema de arquivos.

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'estaticos'),
    os.path.join(BASE_DIR, 'produto/estaticos'),
    '/usr/var/www/html/static'
]

Como visto, com este finder é possível mapear diretórios que estejam na raiz do projeto ou em qualquer outro lugar do sistema de arquivos.

A classe para ativação do componente é a seguinte: django.contrib.staticfiles.finders.FileSystemFinder.

2.1.2. Componente Storage

Outro aspecto importante do app staticfiles é o local de armazenamento dos arquivos estáticos, já que isso influencia diretamente a funcionalidade de coleta de arquivos e também a montagem de URL quando usada a tag static. Esta influência será entendida mais adiante, no tópico sobre as funcionalidades do app.

O armazenamento é tarefa de um componente storage, cuja implementação base é a classe StaticFilesStorage. Para ativar um storage no projeto, basta definir o seu nome completamente qualificado na variável STATICFILES_STORAGE, no arquivo settings.py, conforme exemplo a seguir.

STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'

Esta configuração, não é obrigatória, e seu valor padrão é conforme consta no exemplo acima.

A ideia de um storage é armazenar diversos arquivos em um só local, respeitando os caminhos relativos dos arquivos. Dessa forma, ao armazenar um arquivo cliente/script.js em /www/ resultará em /www/cliente/script.js. Assim, caso os arquivos sejam referenciados da forma correta no projeto, o local de armazenamento poderá ser alterado sem problemas.

2.1.2.1. StaticFilesStorage

O componente StaticFilesStorage busca armazenar os arquivos em um diretório disponível no sistema de arquivos da máquina, sendo útil na preparação para implantação em produção de forma manual ou mesmo automatizada através de um script. Seu funcionamento tem base em dois principais parâmetros, são eles: location e base_url.

O parâmetro location define o local de armazenamento do arquivo, assumindo por padrão o valor da configuração STATIC_ROOT, que deve ser definida em settings.py. Já o parâmetro base_url define o prefixo de URL a ser usado para acessar o arquivo a partir do cliente, normalmente um navegador de Internet. Seu valor padrão corresponde à configuração STATIC_URL. Ambos parâmetros serão melhor entendidos na descrição mais abaixo sobre as funcionalidades de coleta e entrega de estáticos.

A classe para ativação do componente é a seguinte: django.contrib.staticfiles.storage.StaticFilesStorage.

2.1.2.2. ManifestStaticFilesStorage

Outro storage que vale ser citado é o ManifestStaticFilesStorage. Nesta implementação gera-se um hash do arquivo baseado em seu conteúdo a fim de anexar este hash ao nome do arquivo. Com isso é possível manter suporte a versões antigas de cada arquivo, ou ainda, forçar o descarte do cache local dos navegadores, visto que o arquivo terá um novo nome.

Como o nome dos arquivos é alterado, o componente cria um mapa entre nome original e o nome com hash para que seja possível resolver os arquivos em tempo de execução. Este mapa é salvo em um arquivo staticfiles.json, gerado durante o processo de coleta. Como citado acima, o componente storage é usado pela tag static para montagem de URL dos arquivos, o que é principalmente importante neste caso, já que é necessária a leitura do mapeamento gerado.

A classe para ativação do componente é a seguinte: django.contrib.staticfiles.storage.ManifestStaticFilesStorage.

2.1.2.3. Customização de Storages

É possível usar storages customizados que façam integração com repositórios remotos, facilitando o processo de implantação em produção. Para isso basta implementar uma extensão de django.core.files.storage.Storage e configurá-la em STATICFILES_STORAGE como já citado anteriormente. Um ponto de partida é a seguinte documentação: https://docs.djangoproject.com/en/1.11/howto/custom-file-storage/.

Há também implementações prontas de terceiros que podem ser testadas conforme consta no seguinte link: https://docs.djangoproject.com/en/1.11/howto/static-files/deployment/#staticfiles-from-cdn

2.2. Funcionalidades

Esta seção segue a discussão sobre algumas das principais funcionalidades relacionadas ao app staticfiles, sendo elas: referência, entrega e coleta de arquivos estáticos.

2.2.1. Referência a Arquivos Estáticos

Ao referenciar arquivos estáticos em um template Django, a ideia é usar os caminhos relativos dos arquivos prefixados com a URL previamente configurada em STATIC_URL, no settings.py. Com isso é possível gerenciar a localização real destes arquivos sem impactos no resto da aplicação.

Nisso deve-se entender que o caminho relativo dos arquivos não é em relação à raiz do projeto, mas sim em relação ao diretório que está para ser vasculhado pelos finders. Por exemplo, caso exista /projeto/estaticos/imagens/logo.jpg e o diretório /projeto/estaticos esteja na lista de procura dos finders, o caminho relativo do arquivo torna-se somente imagens/logo.jpg.

2.2.1.1. Tags de Template

Na prática não se deve usar diretamente a configuração STATIC_URL no template, mas sim utilizar a tag static conforme exemplo a seguir:

{% load static %}
<img src="{% static "imagens/logo.jpg" %}" alt="Logotipo" />

Esta tag faz parte do núcleo do Django, e pode ser usada sem mesmo ativar o app staticfiles. O que acontece é uma alteração de comportamento quando o app é ativado no projeto. Neste caso invés de simplesmente incluir STATIC_URL como prefixo, a tag usa método url() do componente storage para resolver a URL. No caso de StaticFilesStorage o comportamento final acaba sendo o mesmo.

Outra opção é a tag get_static_prefix que deve ser usada da seguinte forma:

{% load static %}
<img src="{% get_static_prefix %}imagens/logo.jpg" alt="Logotipo" />

Ou ainda:

{% load static %}
{% get_static_prefix as prefix %}
<img src="{% prefix %}imagens/logo.jpg" alt="Logotipo" />

Segue referência sobre as tags:

2.2.2. Entrega em Ambiente de Desenvolvimento

Ao usar o servidor embutido do Django, aquele iniciado com manage.py runserver, basta ativar o app staticfiles para que os arquivos encontrados pelos finders sejam entregues pela aplicação. Entretanto isso só funciona em modo debug, onde consta a configuração DEBUG = True no settings.py. Uma forma de ativar a funcionalidade mesmo fora de debug é iniciar o runserver com o parâmetro --insecure.

Caso seja usado um servidor diferente do runserver é necessário mapear manualmente o padrão de URL dos arquivos estáticos. Este padrão deve ser iniciado com o mesmo prefixo configurado em STATIC_URL e seguir com uma variável de caminho para capturar o caminho relativo do arquivo estático. Segue exemplo onde /static/ é o prefixo e path é a variável que irá capturar o caminho do arquivo:

from django.contrib.staticfiles import views

urlpatterns += [
    url(r'^static/(?P<path>.*)$', views.serve)
]

Preferencialmente deve-se usar a função utilitária staticfiles_urlpatterns que cria o mapeamento automaticamente com base na configuração STATIC_URL. O retorno desta função é uma lista, portanto pode ser usada da seguinte forma:

from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns += staticfiles_urlpatterns()

2.2.2.1. Detalhamento

Como visto no exemplo mais acima, o mapeamento do conteúdo estático é resolvido pela função django.contrib.staticfiles.views.serve que faz parte do app staticfiles. Internamente esta função utiliza os finders para encontrar os arquivos e então entrega o arquivo através da função django.views.static.serve, que faz parte do núcleo do Django. O fato destas funções serem homônimas pode ser um pouco confuso ao analisar o código-fonte, por isso vale a pena o destaque.

Vale citar ainda que a função django.views.static.serve na verdade não tem restrições de debug, podendo então ser usada em qualquer situação. Contudo, comentários no código-fonte novamente alertam que seu uso é voltado ao ambiente de desenvolvimento. A nível de conhecimento, segue exemplo de uso:

from django.views.static import serve

urlpatterns = [
    url(
        r'^static/(?P<path>.*)$',
        serve,
        { 'document_root': '/diretorio/estaticos', 'show_indexes': True }
    )
]

O parâmetro document_root define o diretório onde os estáticos estão armazenados. Já show_indexes é opcional e seu uso permite a listagem dos arquivos a partir do diretório definido em document_root. Isso pode ser útil para disponibilizar rapidamente um navegador de arquivos para usuários do sistema, tal como mapear algum diretório com arquivos disponíveis para download.

2.2.3. Coleta de Arquivos para Deploy

A coleta de arquivos é um utilitário que permite reunir em um só local todos os arquivos estáticos do projeto. Seu funcionamento se baseia nos componentes finder e storage, onde todos os arquivos encontrados pelos finders são passados ao storage para armazenamento.

Por exemplo, caso StaticFilesStorage esteja ativo no projeto, todos os arquivos serão armazenados no diretório definido na configuração STATIC_ROOT do settings.py, conforme funcionamento descrito anteriormente.

O utilitário de coleta deve ser acionado manualmente pelo console conforme exemplo a seguir:

./manage.py collectstatic

O utilitário inclui alguns parâmetros de entrada, onde destacam-se os seguintes:

  • --clear : Remove todos os arquivos do diretório alvo antes de iniciar a cópia.
  • --noinput : Não pergunta nada no terminal. Isso é útil quando deseja-se incluir a coleta em um script de automação.

Segue referência à documentação do utilitário:
https://docs.djangoproject.com/en/1.11/ref/contrib/staticfiles/#django-admin-collectstatic

3. Arquivos de Mídia

Ao falar sobre arquivos estáticos no Django cabe também discutir sobre arquivos de mídia (media), já que alguns aspectos são similares e outros apenas parecem ser.

Primeiro deve-se entender que arquivos estáticos referem-se ao conteúdo criado no desenvolvimento da aplicação e que continuará o mesmo durante a execução da aplicação em produção. Nisso entram scripts de página, folhas de estilo, etc. Por outro lado, arquivos de mídia são aquele conteúdo inserido por usuários da aplicação durante sua execução e que poderá ser removido ou alterado pelos próprios usuários.

Dessa forma, um exemplo que não se aplica aos arquivos de mídia é o utilitário de coleta, já que em desenvolvimento, a princípio, os arquivos de mídia nem existirão.

3.1. Referência aos arquivos de mídia

A referência a arquivos de mídia segue a mesma linha dos arquivos estáticos, sendo necessário definir um prefixo nas configurações do projeto e utilizá-lo ao fazer referência aos arquivos.

O prefixo de mídia deve ser feito na configuração MEDIA_URL, onde da mesma forma que nos estáticos, pode-se utilizar um prefixo relativo, como /media/, ou uma URL completa, como http://cdn.com/media/.

Para referenciar os arquivos a partir de templates do Django, preferencialmente deve-se usar a tag get_media_prefix que retorna o prefixo configurado no projeto de forma similar à tag get_static_prefix. Segue exemplo:

{% load static %}
<a href="{% get_media_prefix %}/some-document.pdf">Download</a>

3.2. Armazenamento dos arquivos de mídia

Da mesma forma que há STATIC_ROOT há a configuração MEDIA_ROOT, porém esta não é de todo igual à sua contraparte.

Enquanto STATIC_ROOT define um diretório que normalmente é removido e recriado a cada nova implantação em produção, MEDIA_ROOT define um diretório cujo conteúdo não pode ser perdido, pois normalmente contém dados gerados pelos usuários da aplicação.

O diretório de mídia deve ser encarado como um banco de dados, visto que deve ser persistente ao longo das implantações da aplicação e deve ser compartilhado, caso a aplicação venha a ser executada em múltiplas instâncias.

3.3. Entrega dos arquivos de mídia

Para entrega destes arquivos deve-se seguir as mesmas ideias dos arquivos estáticos, servindo os arquivos de mídia armazenados no diretório configurado em MEDIA_ROOT a partir do prefixo de URL configurado em MEDIA_URL.

Um detalhe está no app staticfiles, que não irá acionar a entrega dos arquivos de mídia automaticamente no ambiente de desenvolvimento. Por isso será sempre necessário o mapeamento manual, conforme explicado na seção específica mais acima.

4. Implantação em Produção

A documentação do Django deixa claro que o seu utilitário runserver, é voltado apenas ao uso em desenvolvimento, não tendo suficiente performance e segurança para produção. Sabendo disso, normalmente usa-se algum servidor que implemente a especificação WSGI, tal como uWSGI e Gunicorn.

Quanto aos arquivos estáticos, são possíveis duas abordagens: servi-los a partir da mesma URL da aplicação Django, e servi-los a partir de outro domínio e/ou porta de comunicação.

4.1. Implantação com URLs distintas

Usar URLs distintas facilita a separação de responsabilidades na entrega de conteúdo, ficando mais trivial manter processamento dinâmico e estático em máquinas diferentes. Uma aplicação prática disso está na contratação de uma CDN para atender a cenários mais exigentes. Com isso encontra-se uma arquitetura similar à ilustração abaixo.

Arquitetura com ambientes separados para conteúdo estático e dinâmico

Nesta configuração o fluxo normalmente se inicia com o cliente requisitando conteúdo dinâmico à aplicação Django, que por sua vez retorna um HTML renderizado dinamicamente. A partir deste HTML o cliente carrega o conteúdo estático disponível em outra URL, normalmente resolvida por outra unidade computacional.

Como visto anteriormente, basta que o conteúdo estático seja corretamente referenciado nos templates que a real localização destes recursos será indiferente na hora de implantar a aplicação.

4.2. Implantação com única URL

Para usar uma única URL base que seja comum a todos os recursos da aplicação é necessário algum tipo de proxy reverso que faça a distinção entre chamadas a recursos estáticos e recursos dinâmicos. Esta distinção normalmente é feita pelo prefixo da URL.

Como exemplo de implantação pode-se usar um servidor web que funcione como proxy e seja responsável por entregar o conteúdo estático, repassando à aplicação Django somente as outras requisições. O diagrama abaixo ilustra a arquitetura.

Arquitetura com único ambiente para conteúdo estático e dinâmico

No diagrama acima ambos servidor web e WSGI são mantidos em uma mesma máquina e comunicam-se através de socket local, porém é possível alocar o WSGI em outra máquina e fazer o serviço de proxy por comunicação através da rede.

4.3. Contêiners Docker

Para usuários de Docker que buscam a abordagem de servidor web e WSGI em uma única máquina, uma boa referência é a seguinte imagem: https://hub.docker.com/r/tiangolo/uwsgi-nginx.

Esta imagem traz um ambiente funcional que inclui Nginx e uWSGI. Basta copiar os arquivos do projeto Django e instalar as respectivas dependências Python usando o utilitário Pip. Deve-se notar que esta não é uma imagem oficial do Docker, então pode ser necessária uma melhor avaliação a depender dos requisitos do projeto a ser implantado.

Para outros casos pode-se partir da imagem Python oficial disponível em: https://hub.docker.com/_/python

Existe também uma imagem oficial do Django, porém esta consta como depreciada em favor da imagem do Python citada acima. Segue referência: https://hub.docker.com/_/django

5. Conclusão

A abordagem do framework Django quanto ao conteúdo estático pode causar alguma confusão aos iniciantes, mas deve ser considerada no mínimo franca. Isso porque explicitamente abstrai do projeto funcionalidades que já são amplamente e competentemente atendidas por outras soluções.

Dessa forma cabe mesmo ao desenvolvedor empenhar algum esforço para entender melhor a proposta do framework e com isso conseguir entregar uma aplicação com melhor performance e segurança aos seus usuários.

Apêndice 1 – Tabela de Configurações

Configuração Descrição
DEBUG Ativa/desativa o modo de debug da aplicação.
INSTALLED_APPS Aplicativos instalados no projeto. É onde o módulo django.contrib.staticfiles do app staticfiles deve ser incluído para ativação.
MEDIA_URL Define o prefixo de URL para referência a arquivos de mídia (uploads).
MEDIA_ROOT Define o diretório onde serão armazenados os arquivos de mídia (uploads).
STATIC_ROOT Define o diretório onde serão armazenados os arquivos estáticos coletados.
STATIC_URL Define o prefixo de URL para referência aos arquivos estáticos.
STATICFILES_DIRS Define os diretórios que serão vasculhados pelo FileSystemFinder.
STATICFILES_FINDERS Define os finders ativos no projeto para procura de arquivos estáticos.
STATICFILES_STORAGE Define o storage ativo no projeto para armazenamento de estáticos.